diff --git a/README.md b/README.md index 3bc597b..7647407 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index 2f09c83..6273d91 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -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 | diff --git a/docs/repository-navigation-contract.md b/docs/repository-navigation-contract.md new file mode 100644 index 0000000..d049f00 --- /dev/null +++ b/docs/repository-navigation-contract.md @@ -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. diff --git a/hub_core/contracts/__init__.py b/hub_core/contracts/__init__.py index b2008c5..53f8ae3 100644 --- a/hub_core/contracts/__init__.py +++ b/hub_core/contracts/__init__.py @@ -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", +] diff --git a/hub_core/contracts/repository_navigation/__init__.py b/hub_core/contracts/repository_navigation/__init__.py new file mode 100644 index 0000000..4805cb9 --- /dev/null +++ b/hub_core/contracts/repository_navigation/__init__.py @@ -0,0 +1 @@ +"""Versioned repository-navigation contracts.""" diff --git a/hub_core/contracts/repository_navigation/v1_0_0/README.md b/hub_core/contracts/repository_navigation/v1_0_0/README.md new file mode 100644 index 0000000..2f4a58c --- /dev/null +++ b/hub_core/contracts/repository_navigation/v1_0_0/README.md @@ -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. diff --git a/hub_core/contracts/repository_navigation/v1_0_0/__init__.py b/hub_core/contracts/repository_navigation/v1_0_0/__init__.py new file mode 100644 index 0000000..d795427 --- /dev/null +++ b/hub_core/contracts/repository_navigation/v1_0_0/__init__.py @@ -0,0 +1 @@ +"""Repository-navigation contract version 1.0.0.""" diff --git a/hub_core/contracts/repository_navigation/v1_0_0/compatibility-matrix.json b/hub_core/contracts/repository_navigation/v1_0_0/compatibility-matrix.json new file mode 100644 index 0000000..d330136 --- /dev/null +++ b/hub_core/contracts/repository_navigation/v1_0_0/compatibility-matrix.json @@ -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." + } +} diff --git a/hub_core/contracts/repository_navigation/v1_0_0/fixtures/repository-classification-page.json b/hub_core/contracts/repository_navigation/v1_0_0/fixtures/repository-classification-page.json new file mode 100644 index 0000000..b44e224 --- /dev/null +++ b/hub_core/contracts/repository_navigation/v1_0_0/fixtures/repository-classification-page.json @@ -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": [] +} diff --git a/hub_core/contracts/repository_navigation/v1_0_0/fixtures/repository-navigation-projection.json b/hub_core/contracts/repository_navigation/v1_0_0/fixtures/repository-navigation-projection.json new file mode 100644 index 0000000..a8df146 --- /dev/null +++ b/hub_core/contracts/repository_navigation/v1_0_0/fixtures/repository-navigation-projection.json @@ -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 +} diff --git a/hub_core/contracts/repository_navigation/v1_0_0/openapi/repository-navigation.openapi.json b/hub_core/contracts/repository_navigation/v1_0_0/openapi/repository-navigation.openapi.json new file mode 100644 index 0000000..5a22f17 --- /dev/null +++ b/hub_core/contracts/repository_navigation/v1_0_0/openapi/repository-navigation.openapi.json @@ -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"}}} + } + } + } +} diff --git a/hub_core/contracts/repository_navigation/v1_0_0/schemas/repository-classification-page.schema.json b/hub_core/contracts/repository_navigation/v1_0_0/schemas/repository-classification-page.schema.json new file mode 100644 index 0000000..064faa2 --- /dev/null +++ b/hub_core/contracts/repository_navigation/v1_0_0/schemas/repository-classification-page.schema.json @@ -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"] + } + } +} diff --git a/hub_core/contracts/repository_navigation/v1_0_0/schemas/repository-navigation-projection.schema.json b/hub_core/contracts/repository_navigation/v1_0_0/schemas/repository-navigation-projection.schema.json new file mode 100644 index 0000000..c8081b2 --- /dev/null +++ b/hub_core/contracts/repository_navigation/v1_0_0/schemas/repository-navigation-projection.schema.json @@ -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"]} + } +} diff --git a/tests/test_repository_navigation_contract.py b/tests/test_repository_navigation_contract.py new file mode 100644 index 0000000..5bba40f --- /dev/null +++ b/tests/test_repository_navigation_contract.py @@ -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" diff --git a/workplans/HUB-WP-0006-repository-classification-navigation.md b/workplans/HUB-WP-0006-repository-classification-navigation.md index def6acb..e6a3857 100644 --- a/workplans/HUB-WP-0006-repository-classification-navigation.md +++ b/workplans/HUB-WP-0006-repository-classification-navigation.md @@ -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