feat: freeze repository navigation contract
Some checks failed
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / pytest-smoke (push) Failing after 1s

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a0230c-b06c-7641-808a-e191b6d1da49
This commit is contained in:
tegwick 2026-08-22 00:27:44 +02:00
parent b27de6e5bf
commit a701dc89cf
15 changed files with 1053 additions and 4 deletions

View file

@ -42,6 +42,12 @@ from hub_core.contracts import CONTRACT_VERSION, extension_contract_root
contract_root = extension_contract_root()
```
The wheel also includes the frozen `helixforge.repository-navigation` 1.0.0
receiving and query contract. Its packaged schemas, fixtures, compatibility
matrix, and read-only OpenAPI surface are located with
`repository_navigation_contract_root()`; normative rebuild and cursor rules
are in `docs/repository-navigation-contract.md`.
## Runtime scaffold
Install the runtime extra and start the API, MCP, or migration process through
@ -105,6 +111,8 @@ are tracked in `HUB-WP-0006`.
repo, DoI, TPSC/GDPR, risk/alert, and progress tools.
- Packaged `helixforge.hub-extension` 0.1.0 Tier 1 schemas, OpenAPI port
fragments, event catalog, compatibility matrix, and ops-hub fixture.
- Packaged `helixforge.repository-navigation` 1.0.0 schemas, fixtures,
compatibility policy, and read-only projection query contract.
- Injectable primary runtime scaffold with five named ports, health/readiness,
API/MCP/migration commands, and a locked non-root OCI image.

View file

@ -39,7 +39,7 @@
| task | HUB-WP-0005-T05 | done | — | workplans/HUB-WP-0005-core-hub-absorption-execution.md |
| task | HUB-WP-0005-T06 | done | — | workplans/HUB-WP-0005-core-hub-absorption-execution.md |
| task | HUB-WP-0006-T01 | done | — | workplans/HUB-WP-0006-repository-classification-navigation.md |
| task | HUB-WP-0006-T02 | todo | — | workplans/HUB-WP-0006-repository-classification-navigation.md |
| task | HUB-WP-0006-T02 | done | — | workplans/HUB-WP-0006-repository-classification-navigation.md |
| task | HUB-WP-0006-T03 | todo | — | workplans/HUB-WP-0006-repository-classification-navigation.md |
| task | HUB-WP-0006-T04 | todo | — | workplans/HUB-WP-0006-repository-classification-navigation.md |
| task | HUB-WP-0006-T05 | todo | — | workplans/HUB-WP-0006-repository-classification-navigation.md |

View file

@ -0,0 +1,79 @@
# Repository navigation contract 1.0.0
Status: frozen for HUB-WP-0006 T02 on 2026-08-22.
The packaged contract is `helixforge.repository-navigation` version `1.0.0`.
It consumes only `helixforge.repository-classification-projection` version
`1.0.0`, representing `RMGR-CONTRACT-CLASSIFICATION-0001` version `1.0`.
Unsupported versions are rejected; hub-core does not coerce them.
## Authority and identity
Repository `.repo-classification.yaml` files remain authoritative. Repo Manager
validates and publishes them. Hub-core owns only rebuildable navigation rows,
facets, query cursors, diagnostics, and temporary compatibility aliases.
Every change carries the registrar's stable `repository_id` UUID. Slugs may
change and are display/routing labels, not identity. The Repo Manager adapter
adds that UUID when converting its local observation into this envelope.
`locations`, `repo_root`, work-record bodies, credentials, and other host-local
or authority-bearing data are forbidden by the closed schemas.
## Pages, snapshots, and changes
All pages in a transfer have the same `snapshot_id`, `mode`, `generated_at`,
`source_revision`, and `total_repository_count`. Entries are ordered by
`repository_id`; duplicate IDs in one transfer are invalid.
- The first page has a null `page_cursor`.
- `next_cursor` is null exactly when `final_page` is true.
- Cursors are opaque, signed or equivalently integrity-protected, bound to the
snapshot and normalized filter set, and never parsed by clients.
- A full snapshot consists of upserts. Absence means deletion only after every
page validates and the final page is accepted.
- An incremental snapshot contains explicit idempotent upsert/delete changes.
A change is newer only by its source observation/revision semantics; duplicate
delivery has no effect.
- `total_repository_count` is required for a full snapshot and may be null for
incremental delivery.
Activation is atomic. Hub-core builds into a new generation, derives all six
facet kinds, computes SHA-256 over canonical JSON repository rows ordered by
UUID, and swaps the active generation only after the complete transfer passes
schema and semantic validation. A failed full rebuild never deletes or partly
replaces the last accepted generation.
## Failure and diagnostics
An error diagnostic, malformed page, inconsistent snapshot metadata,
unsupported version, duplicate identity, invalid controlled value, or upstream
failure rejects that transfer. The last accepted projection remains readable
as `stale`, includes a diagnostic and `source_checked_at`, and readiness reports
the Repo Manager dependency as degraded. If no accepted generation exists, the
query port returns `503`. Successful acceptance restores `current`.
Provenance on every repository includes the Repo Manager URI, classification
contract version, source fingerprint, observed time, and repository revision.
The response also carries the source snapshot/revision and rebuild content
hash. Diagnostics must not include secrets or unbounded source content.
## Query semantics
Repositories are returned in ascending UUID order. Repeated values within one
filter family are ORed; different filter families are ANDed. The six families
are primary domain, secondary domain, category, capability tag, business stake,
and business mechanic. An unknown value produces `400`; an empty match is a
successful empty page.
Response cursors are bound to both the active content hash and normalized
filters. Reusing a cursor after a rebuild or with different filters returns
`409 cursor_snapshot_mismatch`, so a client restarts from the first page.
## Legacy topic aliases
`legacy_topic_aliases` is temporary, read-only compatibility metadata. Each
entry maps one State Hub topic UUID/slug to one derived facet and must name an
`expires_at` time and `residual_owner`. It cannot create, update, or delete
classification. Expired aliases return `410` until removed; there are no write
operations in the navigation OpenAPI surface. STATE-WP-0079 owns alias expiry
and the consumer route switch.

View file

@ -5,6 +5,8 @@ from importlib.resources.abc import Traversable
CONTRACT_ID = "helixforge.hub-extension"
CONTRACT_VERSION = "0.1.0"
REPOSITORY_NAVIGATION_CONTRACT_ID = "helixforge.repository-navigation"
REPOSITORY_NAVIGATION_CONTRACT_VERSION = "1.0.0"
def extension_contract_root() -> Traversable:
@ -13,4 +15,17 @@ def extension_contract_root() -> Traversable:
return files("hub_core.contracts.helixforge_hub_extension.v0_1_0")
__all__ = ["CONTRACT_ID", "CONTRACT_VERSION", "extension_contract_root"]
def repository_navigation_contract_root() -> Traversable:
"""Return the packaged root for the repository-navigation contract."""
return files("hub_core.contracts.repository_navigation.v1_0_0")
__all__ = [
"CONTRACT_ID",
"CONTRACT_VERSION",
"REPOSITORY_NAVIGATION_CONTRACT_ID",
"REPOSITORY_NAVIGATION_CONTRACT_VERSION",
"extension_contract_root",
"repository_navigation_contract_root",
]

View file

@ -0,0 +1 @@
"""Versioned repository-navigation contracts."""

View file

@ -0,0 +1,24 @@
# helixforge.repository-navigation 1.0.0
This package freezes hub-core's receiving and read contracts for Repo Manager
classification projections:
- `schemas/repository-classification-page.schema.json` validates full and
incremental input pages;
- `schemas/repository-navigation-projection.schema.json` defines the derived
repository, facet, provenance, diagnostic, cursor, and legacy-alias output;
- `openapi/repository-navigation.openapi.json` defines the read-only
`port.projection.query` routes;
- `fixtures/` supplies a valid full input page and its representative output;
- `compatibility-matrix.json` records the only accepted input version and the
temporary State Hub topic adapter.
Repository files are classification authority and Repo Manager is the
validator/publisher. Hub-core requires the registrar's stable repository UUID,
derives indexes, and never accepts classification writes. Host-local paths and
work-record contents do not cross this contract.
Normative cursor, rebuild, failure, filtering, provenance, and alias rules are
documented in `docs/repository-navigation-contract.md`. Breaking authority,
identity, deletion, rebuild, cursor, or facet changes require a new major
contract and a dual-run window.

View file

@ -0,0 +1 @@
"""Repository-navigation contract version 1.0.0."""

View file

@ -0,0 +1,38 @@
{
"contract_id": "helixforge.repository-navigation",
"current_version": "1.0.0",
"accepted_inputs": [
{
"contract_id": "helixforge.repository-classification-projection",
"version": "1.0.0",
"classification_contract_id": "RMGR-CONTRACT-CLASSIFICATION-0001",
"classification_contract_version": "1.0",
"status": "current",
"coercion": false
}
],
"versions": [
{
"version": "1.0.0",
"status": "current",
"compatible_min": "1.0.0",
"compatible_max": "1.0.0",
"breaking": false
}
],
"legacy_adapters": [
{
"source": "state-hub.topic",
"target": "legacy_topic_aliases",
"status": "temporary-read-only",
"authority": false,
"required_metadata": ["expires_at", "residual_owner"],
"notes": "An alias points to one derived facet. It cannot create or mutate repository classification and must be removed after its residual owner completes the cutover."
}
],
"change_policy": {
"patch": "Clarifications and compatible constraint corrections only.",
"minor": "Additive optional fields, query filters, and diagnostics.",
"major": "Changed authority, identity, cursor, rebuild, deletion, facet, or compatibility semantics; removed or renamed fields; or narrowed enums. Requires a dual-run window."
}
}

View file

@ -0,0 +1,61 @@
{
"contract_id": "helixforge.repository-classification-projection",
"contract_version": "1.0.0",
"source": {
"system": "repo-manager",
"classification_contract_id": "RMGR-CONTRACT-CLASSIFICATION-0001",
"classification_contract_version": "1.0",
"producer_version": "0.4.0"
},
"snapshot": {
"snapshot_id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"mode": "full",
"generated_at": "2026-08-22T12:00:00Z",
"source_revision": "0123456789abcdef0123456789abcdef01234567",
"page_cursor": null,
"next_cursor": null,
"final_page": true,
"total_repository_count": 2
},
"repositories": [
{
"operation": "upsert",
"repository_id": "11111111-1111-4111-8111-111111111111",
"slug": "hub-core",
"lifecycle": "active",
"classification": {
"category": "tooling",
"domain": "infotech",
"secondary_domains": ["agents"],
"capability_tags": ["hub-runtime", "repository-navigation"],
"business_stake": ["technology"],
"business_mechanics": ["coordination"]
},
"revision": {
"head_sha": "1111111111111111111111111111111111111111",
"source_fingerprint": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"observed_at": "2026-08-22T11:59:00Z"
}
},
{
"operation": "upsert",
"repository_id": "22222222-2222-4222-8222-222222222222",
"slug": "repo-manager",
"lifecycle": "active",
"classification": {
"category": "tooling",
"domain": "infotech",
"secondary_domains": [],
"capability_tags": ["repository-governance"],
"business_stake": ["operations", "technology"],
"business_mechanics": ["control", "coordination"]
},
"revision": {
"head_sha": "2222222222222222222222222222222222222222",
"source_fingerprint": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
"observed_at": "2026-08-22T11:59:30Z"
}
}
],
"diagnostics": []
}

View file

@ -0,0 +1,121 @@
{
"contract_id": "helixforge.repository-navigation",
"contract_version": "1.0.0",
"projection_id": "repository_navigation",
"projection_status": "current",
"source_snapshot": {
"snapshot_id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"source_system": "repo-manager",
"source_contract_id": "RMGR-CONTRACT-CLASSIFICATION-0001",
"source_contract_version": "1.0",
"source_revision": "0123456789abcdef0123456789abcdef01234567",
"generated_at": "2026-08-22T12:00:00Z"
},
"source_checked_at": "2026-08-22T12:00:03Z",
"rebuilt_at": "2026-08-22T12:00:02Z",
"content_hash": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
"repositories": [
{
"repository_id": "11111111-1111-4111-8111-111111111111",
"slug": "hub-core",
"lifecycle": "active",
"primary_domain": "infotech",
"secondary_domains": ["agents"],
"category": "tooling",
"capability_tags": ["hub-runtime", "repository-navigation"],
"business_stake": ["technology"],
"business_mechanics": ["coordination"],
"provenance": {
"source_system": "repo-manager",
"source_ref": "repo-manager://repositories/11111111-1111-4111-8111-111111111111",
"classification_contract_version": "1.0",
"repository_revision": "1111111111111111111111111111111111111111",
"source_fingerprint": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"observed_at": "2026-08-22T11:59:00Z"
}
},
{
"repository_id": "22222222-2222-4222-8222-222222222222",
"slug": "repo-manager",
"lifecycle": "active",
"primary_domain": "infotech",
"secondary_domains": [],
"category": "tooling",
"capability_tags": ["repository-governance"],
"business_stake": ["operations", "technology"],
"business_mechanics": ["control", "coordination"],
"provenance": {
"source_system": "repo-manager",
"source_ref": "repo-manager://repositories/22222222-2222-4222-8222-222222222222",
"classification_contract_version": "1.0",
"repository_revision": "2222222222222222222222222222222222222222",
"source_fingerprint": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
"observed_at": "2026-08-22T11:59:30Z"
}
}
],
"facets": [
{
"kind": "primary_domain",
"value": "infotech",
"repository_count": 2,
"repository_ids": [
"11111111-1111-4111-8111-111111111111",
"22222222-2222-4222-8222-222222222222"
]
},
{
"kind": "secondary_domain",
"value": "agents",
"repository_count": 1,
"repository_ids": ["11111111-1111-4111-8111-111111111111"]
},
{
"kind": "category",
"value": "tooling",
"repository_count": 2,
"repository_ids": [
"11111111-1111-4111-8111-111111111111",
"22222222-2222-4222-8222-222222222222"
]
},
{
"kind": "capability_tag",
"value": "repository-navigation",
"repository_count": 1,
"repository_ids": ["11111111-1111-4111-8111-111111111111"]
},
{
"kind": "business_stake",
"value": "technology",
"repository_count": 2,
"repository_ids": [
"11111111-1111-4111-8111-111111111111",
"22222222-2222-4222-8222-222222222222"
]
},
{
"kind": "business_mechanic",
"value": "coordination",
"repository_count": 2,
"repository_ids": [
"11111111-1111-4111-8111-111111111111",
"22222222-2222-4222-8222-222222222222"
]
}
],
"legacy_topic_aliases": [
{
"legacy_topic_id": "33333333-3333-4333-8333-333333333333",
"legacy_topic_slug": "infotech",
"facet_kind": "primary_domain",
"facet_value": "infotech",
"status": "active",
"expires_at": "2026-09-30T23:59:59Z",
"residual_owner": "STATE-WP-0079"
}
],
"diagnostics": [],
"next_cursor": null,
"total_repository_count": 2
}

View file

@ -0,0 +1,156 @@
{
"openapi": "3.1.0",
"info": {
"title": "HelixForge repository navigation projection",
"version": "1.0.0"
},
"paths": {
"/ports/projections/repository-navigation/repositories": {
"get": {
"operationId": "queryRepositoryNavigation",
"x-port-id": "port.projection.query",
"x-direction": "out",
"summary": "Query the derived repository navigation projection",
"parameters": [
{"$ref": "#/components/parameters/PrimaryDomain"},
{"$ref": "#/components/parameters/SecondaryDomain"},
{"$ref": "#/components/parameters/Category"},
{"$ref": "#/components/parameters/CapabilityTag"},
{"$ref": "#/components/parameters/BusinessStake"},
{"$ref": "#/components/parameters/BusinessMechanic"},
{"$ref": "#/components/parameters/Cursor"},
{"$ref": "#/components/parameters/Limit"}
],
"responses": {
"200": {
"description": "Snapshot-bound page in stable repository UUID order",
"content": {
"application/json": {
"schema": {"$ref": "../schemas/repository-navigation-projection.schema.json"}
}
}
},
"400": {"$ref": "#/components/responses/BadRequest"},
"409": {"$ref": "#/components/responses/CursorMismatch"},
"503": {"$ref": "#/components/responses/DependencyUnavailable"}
}
}
},
"/ports/projections/repository-navigation/facets/{facet_kind}/{facet_value}": {
"get": {
"operationId": "getRepositoryNavigationFacet",
"x-port-id": "port.projection.query",
"x-direction": "out",
"summary": "Resolve one derived navigation facet",
"parameters": [
{
"name": "facet_kind",
"in": "path",
"required": true,
"schema": {"$ref": "#/components/schemas/FacetKind"}
},
{
"name": "facet_value",
"in": "path",
"required": true,
"schema": {"type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"}
},
{"$ref": "#/components/parameters/Cursor"},
{"$ref": "#/components/parameters/Limit"}
],
"responses": {
"200": {
"description": "Repositories in the selected derived facet",
"content": {
"application/json": {
"schema": {"$ref": "../schemas/repository-navigation-projection.schema.json"}
}
}
},
"400": {"$ref": "#/components/responses/BadRequest"},
"404": {"description": "Facet not found"},
"409": {"$ref": "#/components/responses/CursorMismatch"},
"503": {"$ref": "#/components/responses/DependencyUnavailable"}
}
}
},
"/ports/projections/repository-navigation/legacy-topic-aliases/{legacy_topic_id}": {
"get": {
"operationId": "resolveLegacyRepositoryTopicAlias",
"x-port-id": "port.projection.query",
"x-direction": "out",
"summary": "Resolve temporary read-only State Hub topic compatibility metadata",
"parameters": [
{
"name": "legacy_topic_id",
"in": "path",
"required": true,
"schema": {"type": "string", "format": "uuid"}
}
],
"responses": {
"200": {
"description": "Active alias and its derived facet target",
"content": {
"application/json": {
"schema": {
"$ref": "../schemas/repository-navigation-projection.schema.json#/$defs/legacyTopicAlias"
}
}
}
},
"404": {"description": "Alias not found"},
"410": {"description": "Alias expired; response identifies the residual owner"}
}
}
}
},
"components": {
"schemas": {
"FacetKind": {
"enum": [
"primary_domain",
"secondary_domain",
"category",
"capability_tag",
"business_stake",
"business_mechanic"
]
},
"Error": {
"type": "object",
"additionalProperties": false,
"required": ["code", "message"],
"properties": {
"code": {"type": "string"},
"message": {"type": "string"},
"retryable": {"type": "boolean"}
}
}
},
"parameters": {
"PrimaryDomain": {"name": "primary_domain", "in": "query", "schema": {"type": "array", "items": {"type": "string"}}, "explode": true},
"SecondaryDomain": {"name": "secondary_domain", "in": "query", "schema": {"type": "array", "items": {"type": "string"}}, "explode": true},
"Category": {"name": "category", "in": "query", "schema": {"type": "array", "items": {"type": "string"}}, "explode": true},
"CapabilityTag": {"name": "capability_tag", "in": "query", "schema": {"type": "array", "items": {"type": "string"}}, "explode": true},
"BusinessStake": {"name": "business_stake", "in": "query", "schema": {"type": "array", "items": {"type": "string"}}, "explode": true},
"BusinessMechanic": {"name": "business_mechanic", "in": "query", "schema": {"type": "array", "items": {"type": "string"}}, "explode": true},
"Cursor": {"name": "cursor", "in": "query", "schema": {"type": "string", "maxLength": 1000}},
"Limit": {"name": "limit", "in": "query", "schema": {"type": "integer", "minimum": 1, "maximum": 500, "default": 100}}
},
"responses": {
"BadRequest": {
"description": "Invalid filter or cursor",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
},
"CursorMismatch": {
"description": "Cursor belongs to a superseded projection snapshot",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
},
"DependencyUnavailable": {
"description": "No accepted projection is available",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
}
}
}
}

View file

@ -0,0 +1,150 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://schemas.helixforge.local/repository-navigation/1.0.0/repository-classification-page.schema.json",
"title": "Repo Manager classification projection page",
"type": "object",
"additionalProperties": false,
"required": [
"contract_id",
"contract_version",
"source",
"snapshot",
"repositories",
"diagnostics"
],
"properties": {
"contract_id": {"const": "helixforge.repository-classification-projection"},
"contract_version": {"const": "1.0.0"},
"source": {"$ref": "#/$defs/source"},
"snapshot": {"$ref": "#/$defs/snapshot"},
"repositories": {
"type": "array",
"items": {"$ref": "#/$defs/change"}
},
"diagnostics": {
"type": "array",
"items": {"$ref": "#/$defs/diagnostic"}
}
},
"$defs": {
"source": {
"type": "object",
"additionalProperties": false,
"required": [
"system",
"classification_contract_id",
"classification_contract_version",
"producer_version"
],
"properties": {
"system": {"const": "repo-manager"},
"classification_contract_id": {"const": "RMGR-CONTRACT-CLASSIFICATION-0001"},
"classification_contract_version": {"const": "1.0"},
"producer_version": {"type": "string", "minLength": 1, "maxLength": 80}
}
},
"snapshot": {
"type": "object",
"additionalProperties": false,
"required": [
"snapshot_id",
"mode",
"generated_at",
"source_revision",
"page_cursor",
"next_cursor",
"final_page",
"total_repository_count"
],
"properties": {
"snapshot_id": {"type": "string", "pattern": "^[a-f0-9]{64}$"},
"mode": {"enum": ["full", "incremental"]},
"generated_at": {"type": "string", "format": "date-time"},
"source_revision": {"type": "string", "pattern": "^[a-f0-9]{7,64}$"},
"page_cursor": {"type": ["string", "null"], "maxLength": 1000},
"next_cursor": {"type": ["string", "null"], "maxLength": 1000},
"final_page": {"type": "boolean"},
"total_repository_count": {"type": ["integer", "null"], "minimum": 0}
}
},
"revision": {
"type": "object",
"additionalProperties": false,
"required": ["head_sha", "source_fingerprint", "observed_at"],
"properties": {
"head_sha": {"type": ["string", "null"], "pattern": "^[a-f0-9]{40,64}$"},
"source_fingerprint": {"type": "string", "pattern": "^[a-f0-9]{64}$"},
"observed_at": {"type": "string", "format": "date-time"}
}
},
"classification": {
"type": "object",
"additionalProperties": false,
"required": ["category", "domain"],
"properties": {
"category": {"enum": ["experimental", "research", "project", "tooling", "product", "business"]},
"domain": {"$ref": "#/$defs/domain"},
"secondary_domains": {
"type": "array",
"items": {"$ref": "#/$defs/domain"},
"uniqueItems": true
},
"capability_tags": {
"type": "array",
"items": {"type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$", "maxLength": 120},
"uniqueItems": true
},
"business_stake": {
"type": "array",
"items": {"enum": ["execution", "intelligence", "finance", "legal", "sales", "experience", "technology", "operations", "product", "people", "procurement", "sustainability", "automation"]},
"uniqueItems": true
},
"business_mechanics": {
"type": "array",
"items": {"enum": ["intention", "control", "coordination", "operation", "adaptation"]},
"uniqueItems": true
}
}
},
"upsert": {
"type": "object",
"additionalProperties": false,
"required": ["operation", "repository_id", "slug", "lifecycle", "classification", "revision"],
"properties": {
"operation": {"const": "upsert"},
"repository_id": {"type": "string", "format": "uuid"},
"slug": {"type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$", "maxLength": 120},
"lifecycle": {"enum": ["active", "archived", "retired"]},
"classification": {"$ref": "#/$defs/classification"},
"revision": {"$ref": "#/$defs/revision"}
}
},
"delete": {
"type": "object",
"additionalProperties": false,
"required": ["operation", "repository_id", "slug", "revision"],
"properties": {
"operation": {"const": "delete"},
"repository_id": {"type": "string", "format": "uuid"},
"slug": {"type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$", "maxLength": 120},
"revision": {"$ref": "#/$defs/revision"}
}
},
"change": {"oneOf": [{"$ref": "#/$defs/upsert"}, {"$ref": "#/$defs/delete"}]},
"diagnostic": {
"type": "object",
"additionalProperties": false,
"required": ["severity", "code", "message"],
"properties": {
"severity": {"enum": ["info", "warning", "error"]},
"code": {"type": "string", "pattern": "^[a-z][a-z0-9_.-]*$", "maxLength": 120},
"message": {"type": "string", "minLength": 1, "maxLength": 1000},
"repository_id": {"type": "string", "format": "uuid"},
"field": {"type": "string", "maxLength": 160}
}
},
"domain": {
"enum": ["infotech", "financials", "communication", "consumer", "health", "industrials", "energy", "utilities", "materials", "realestate", "crypto", "agents", "space", "government"]
}
}
}

View file

@ -0,0 +1,204 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://schemas.helixforge.local/repository-navigation/1.0.0/repository-navigation-projection.schema.json",
"title": "Hub repository navigation projection",
"type": "object",
"additionalProperties": false,
"required": [
"contract_id",
"contract_version",
"projection_id",
"projection_status",
"source_snapshot",
"source_checked_at",
"rebuilt_at",
"content_hash",
"repositories",
"facets",
"legacy_topic_aliases",
"diagnostics",
"next_cursor",
"total_repository_count"
],
"properties": {
"contract_id": {"const": "helixforge.repository-navigation"},
"contract_version": {"const": "1.0.0"},
"projection_id": {"const": "repository_navigation"},
"projection_status": {"enum": ["current", "stale"]},
"source_snapshot": {"$ref": "#/$defs/sourceSnapshot"},
"source_checked_at": {"type": "string", "format": "date-time"},
"rebuilt_at": {"type": "string", "format": "date-time"},
"content_hash": {"type": "string", "pattern": "^[a-f0-9]{64}$"},
"repositories": {
"type": "array",
"items": {"$ref": "#/$defs/repository"}
},
"facets": {
"type": "array",
"items": {"$ref": "#/$defs/facet"}
},
"legacy_topic_aliases": {
"type": "array",
"items": {"$ref": "#/$defs/legacyTopicAlias"}
},
"diagnostics": {
"type": "array",
"items": {"$ref": "#/$defs/diagnostic"}
},
"next_cursor": {"type": ["string", "null"], "maxLength": 1000},
"total_repository_count": {"type": "integer", "minimum": 0}
},
"$defs": {
"sourceSnapshot": {
"type": "object",
"additionalProperties": false,
"required": [
"snapshot_id",
"source_system",
"source_contract_id",
"source_contract_version",
"source_revision",
"generated_at"
],
"properties": {
"snapshot_id": {"type": "string", "pattern": "^[a-f0-9]{64}$"},
"source_system": {"const": "repo-manager"},
"source_contract_id": {"const": "RMGR-CONTRACT-CLASSIFICATION-0001"},
"source_contract_version": {"const": "1.0"},
"source_revision": {"type": "string", "pattern": "^[a-f0-9]{7,64}$"},
"generated_at": {"type": "string", "format": "date-time"}
}
},
"provenance": {
"type": "object",
"additionalProperties": false,
"required": [
"source_system",
"source_ref",
"classification_contract_version",
"repository_revision",
"source_fingerprint",
"observed_at"
],
"properties": {
"source_system": {"const": "repo-manager"},
"source_ref": {"type": "string", "pattern": "^repo-manager://repositories/[0-9a-f-]{36}$"},
"classification_contract_version": {"const": "1.0"},
"repository_revision": {"type": ["string", "null"], "pattern": "^[a-f0-9]{40,64}$"},
"source_fingerprint": {"type": "string", "pattern": "^[a-f0-9]{64}$"},
"observed_at": {"type": "string", "format": "date-time"}
}
},
"repository": {
"type": "object",
"additionalProperties": false,
"required": [
"repository_id",
"slug",
"lifecycle",
"primary_domain",
"secondary_domains",
"category",
"capability_tags",
"business_stake",
"business_mechanics",
"provenance"
],
"properties": {
"repository_id": {"type": "string", "format": "uuid"},
"slug": {"type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$", "maxLength": 120},
"lifecycle": {"enum": ["active", "archived", "retired"]},
"primary_domain": {"$ref": "#/$defs/domain"},
"secondary_domains": {
"type": "array",
"items": {"$ref": "#/$defs/domain"},
"uniqueItems": true
},
"category": {"$ref": "#/$defs/category"},
"capability_tags": {
"type": "array",
"items": {"type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$", "maxLength": 120},
"uniqueItems": true
},
"business_stake": {
"type": "array",
"items": {"$ref": "#/$defs/businessStake"},
"uniqueItems": true
},
"business_mechanics": {
"type": "array",
"items": {"$ref": "#/$defs/businessMechanic"},
"uniqueItems": true
},
"provenance": {"$ref": "#/$defs/provenance"}
}
},
"facetKind": {
"enum": [
"primary_domain",
"secondary_domain",
"category",
"capability_tag",
"business_stake",
"business_mechanic"
]
},
"facet": {
"type": "object",
"additionalProperties": false,
"required": ["kind", "value", "repository_count", "repository_ids"],
"properties": {
"kind": {"$ref": "#/$defs/facetKind"},
"value": {"type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$", "maxLength": 120},
"repository_count": {"type": "integer", "minimum": 0},
"repository_ids": {
"type": "array",
"items": {"type": "string", "format": "uuid"},
"uniqueItems": true
}
}
},
"legacyTopicAlias": {
"type": "object",
"additionalProperties": false,
"required": [
"legacy_topic_id",
"legacy_topic_slug",
"facet_kind",
"facet_value",
"status",
"expires_at",
"residual_owner"
],
"properties": {
"legacy_topic_id": {"type": "string", "format": "uuid"},
"legacy_topic_slug": {"type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$", "maxLength": 120},
"facet_kind": {"$ref": "#/$defs/facetKind"},
"facet_value": {"type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$", "maxLength": 120},
"status": {"enum": ["active", "expired"]},
"expires_at": {"type": "string", "format": "date-time"},
"residual_owner": {"type": "string", "minLength": 1, "maxLength": 160}
}
},
"diagnostic": {
"type": "object",
"additionalProperties": false,
"required": ["severity", "code", "message"],
"properties": {
"severity": {"enum": ["info", "warning", "error"]},
"code": {"type": "string", "pattern": "^[a-z][a-z0-9_.-]*$", "maxLength": 120},
"message": {"type": "string", "minLength": 1, "maxLength": 1000},
"repository_id": {"type": "string", "format": "uuid"},
"field": {"type": "string", "maxLength": 160}
}
},
"category": {"enum": ["experimental", "research", "project", "tooling", "product", "business"]},
"domain": {
"enum": ["infotech", "financials", "communication", "consumer", "health", "industrials", "energy", "utilities", "materials", "realestate", "crypto", "agents", "space", "government"]
},
"businessStake": {
"enum": ["execution", "intelligence", "finance", "legal", "sales", "experience", "technology", "operations", "product", "people", "procurement", "sustainability", "automation"]
},
"businessMechanic": {"enum": ["intention", "control", "coordination", "operation", "adaptation"]}
}
}

View file

@ -0,0 +1,183 @@
import json
from collections.abc import Mapping, Sequence
from typing import Any
from jsonschema import Draft202012Validator, FormatChecker
from hub_core.contracts import (
REPOSITORY_NAVIGATION_CONTRACT_ID,
REPOSITORY_NAVIGATION_CONTRACT_VERSION,
repository_navigation_contract_root,
)
ROOT = repository_navigation_contract_root()
SCHEMAS = ROOT.joinpath("schemas")
INPUT_FIXTURE = ROOT.joinpath("fixtures", "repository-classification-page.json")
OUTPUT_FIXTURE = ROOT.joinpath("fixtures", "repository-navigation-projection.json")
OPENAPI = ROOT.joinpath("openapi", "repository-navigation.openapi.json")
COMPATIBILITY = ROOT.joinpath("compatibility-matrix.json")
FACET_KINDS = {
"primary_domain",
"secondary_domain",
"category",
"capability_tag",
"business_stake",
"business_mechanic",
}
SECRET_OR_LOCAL_KEYS = {
"api_key",
"credential",
"locations",
"password",
"passwd",
"private_key",
"repo_root",
"secret",
"token",
"work_records",
}
def load_json(resource: Any) -> Any:
return json.loads(resource.read_text(encoding="utf-8"))
def validate(instance: Any, schema_name: str) -> None:
schema = load_json(SCHEMAS.joinpath(schema_name))
Draft202012Validator.check_schema(schema)
Draft202012Validator(schema, format_checker=FormatChecker()).validate(instance)
def iter_keys(value: Any) -> set[str]:
if isinstance(value, Mapping):
keys = {str(key).lower() for key in value}
for child in value.values():
keys.update(iter_keys(child))
return keys
if isinstance(value, Sequence) and not isinstance(value, (str, bytes)):
keys: set[str] = set()
for child in value:
keys.update(iter_keys(child))
return keys
return set()
def test_packaged_repository_navigation_contract_identity_and_artifacts() -> None:
assert REPOSITORY_NAVIGATION_CONTRACT_ID == "helixforge.repository-navigation"
assert REPOSITORY_NAVIGATION_CONTRACT_VERSION == "1.0.0"
for resource in (
ROOT.joinpath("README.md"),
INPUT_FIXTURE,
OUTPUT_FIXTURE,
OPENAPI,
COMPATIBILITY,
):
assert resource.is_file()
def test_input_and_output_fixtures_validate() -> None:
validate(load_json(INPUT_FIXTURE), "repository-classification-page.schema.json")
validate(load_json(OUTPUT_FIXTURE), "repository-navigation-projection.schema.json")
def test_input_semantics_use_stable_unique_identity_and_consistent_final_page() -> None:
page = load_json(INPUT_FIXTURE)
repository_ids = [entry["repository_id"] for entry in page["repositories"]]
assert repository_ids == sorted(repository_ids)
assert len(repository_ids) == len(set(repository_ids))
assert page["snapshot"]["final_page"] is (page["snapshot"]["next_cursor"] is None)
assert page["snapshot"]["total_repository_count"] == len(repository_ids)
assert all(
entry["classification"].get("domain")
not in entry["classification"].get("secondary_domains", [])
for entry in page["repositories"]
if entry["operation"] == "upsert"
)
def test_output_has_every_derived_facet_and_per_repository_provenance() -> None:
projection = load_json(OUTPUT_FIXTURE)
repositories = projection["repositories"]
repository_ids = [entry["repository_id"] for entry in repositories]
assert repository_ids == sorted(repository_ids)
assert len(repository_ids) == len(set(repository_ids))
assert {facet["kind"] for facet in projection["facets"]} == FACET_KINDS
assert projection["total_repository_count"] == len(repositories)
assert all(
entry["provenance"]["source_ref"].endswith(entry["repository_id"])
for entry in repositories
)
assert all(
facet["repository_count"] == len(facet["repository_ids"])
for facet in projection["facets"]
)
def test_contract_examples_exclude_secrets_and_host_local_authority_data() -> None:
assert not (iter_keys(load_json(INPUT_FIXTURE)) & SECRET_OR_LOCAL_KEYS)
assert not (iter_keys(load_json(OUTPUT_FIXTURE)) & SECRET_OR_LOCAL_KEYS)
def test_compatibility_accepts_only_repo_manager_v1_without_coercion() -> None:
matrix = load_json(COMPATIBILITY)
accepted = matrix["accepted_inputs"]
assert matrix["contract_id"] == REPOSITORY_NAVIGATION_CONTRACT_ID
assert matrix["current_version"] == REPOSITORY_NAVIGATION_CONTRACT_VERSION
assert accepted == [
{
"contract_id": "helixforge.repository-classification-projection",
"version": "1.0.0",
"classification_contract_id": "RMGR-CONTRACT-CLASSIFICATION-0001",
"classification_contract_version": "1.0",
"status": "current",
"coercion": False,
}
]
def test_navigation_openapi_is_read_only_and_exposes_all_filter_families() -> None:
document = load_json(OPENAPI)
operations = [
(method, operation)
for path_item in document["paths"].values()
for method, operation in path_item.items()
if method in {"get", "post", "put", "patch", "delete"}
]
query = document["paths"][
"/ports/projections/repository-navigation/repositories"
]["get"]
parameter_names = {
reference["$ref"].rsplit("/", 1)[-1]
for reference in query["parameters"]
if "$ref" in reference
}
assert document["openapi"] == "3.1.0"
assert all(method == "get" for method, _ in operations)
assert all(operation["x-port-id"] == "port.projection.query" for _, operation in operations)
assert {
"PrimaryDomain",
"SecondaryDomain",
"Category",
"CapabilityTag",
"BusinessStake",
"BusinessMechanic",
} <= parameter_names
assert set(document["components"]["schemas"]["FacetKind"]["enum"]) == FACET_KINDS
def test_legacy_topic_alias_is_expiring_read_only_metadata() -> None:
projection = load_json(OUTPUT_FIXTURE)
aliases = projection["legacy_topic_aliases"]
matrix = load_json(COMPATIBILITY)
assert aliases
assert all(alias["expires_at"] and alias["residual_owner"] for alias in aliases)
adapter = matrix["legacy_adapters"][0]
assert adapter["authority"] is False
assert adapter["status"] == "temporary-read-only"

View file

@ -52,7 +52,7 @@ single-authority boundary.
```task
id: HUB-WP-0006-T02
status: todo
status: done
priority: high
state_hub_task_id: "47d7d168-217f-45c6-b4d7-ab9cf4dd5329"
```
@ -62,6 +62,14 @@ identity, cursor/snapshot semantics, provenance, diagnostics, and the derived
domain/category/tag/stake/mechanic views. Define legacy topic aliases as
temporary compatibility metadata, not mutable topic authority.
Completed 2026-08-22. Packaged `helixforge.repository-navigation` 1.0.0
defines the strict Repo Manager page envelope, stable registrar UUID identity,
atomic snapshot/rebuild and cursor rules, per-repository provenance, failure
diagnostics, all six derived facet kinds, read-only query routes, and expiring
State Hub topic aliases with residual ownership. Both representative fixtures
validate, the wheel contains all eight artifacts, and the full suite passes
72 tests.
## Implement Repo Manager ingestion and rebuild
```task
@ -121,7 +129,7 @@ expiry. Wait on T02-T05 and the State Hub cutover window.
## Acceptance
- [x] Repo Manager v1.0 authority and projection contract accepted
- [ ] Hub-side projection and compatibility alias contract versioned
- [x] Hub-side projection and compatibility alias contract versioned
- [ ] Rebuildable durable ingestion passes provenance and failure checks
- [ ] HTTP/MCP navigation reads only the derived projection
- [ ] A5/A4 consumer comparison and rollback evidence recorded