diff --git a/config/repository-metadata.json b/config/repository-metadata.json index bb95527ee7..d324d45085 100644 --- a/config/repository-metadata.json +++ b/config/repository-metadata.json @@ -4,135 +4,729 @@ "repositories": { "CalendarWeave": { "description": "CalendarWeave — governed calendar resources, iCalendar semantics, and interoperable scheduling infrastructure.", - "topics": ["calendar", "caldav", "icalendar", "scheduling", "rust", "contextualwisdomlab"], + "topics": [ + "calendar", + "caldav", + "icalendar", + "scheduling", + "rust", + "contextualwisdomlab" + ], "deepwiki": true, "pages": true }, "ConceptWeave": { "description": "ConceptWeave — turn enterprise data into governed semantic models and reusable meaning.", - "topics": ["semantic-model", "ontology", "knowledge-graph", "data-governance", "rust", "contextualwisdomlab"], + "topics": [ + "semantic-model", + "ontology", + "knowledge-graph", + "data-governance", + "rust", + "contextualwisdomlab" + ], "deepwiki": true, "pages": true }, "context-graph-contracts": { "description": "Context Graph Contracts — versioned interoperability contracts for context, lineage, provenance, and architecture facts.", - "topics": ["interoperability", "json-schema", "asyncapi", "cloudevents", "provenance", "context-graph", "contextualwisdomlab"], + "topics": [ + "interoperability", + "json-schema", + "asyncapi", + "cloudevents", + "provenance", + "context-graph", + "contextualwisdomlab" + ], "deepwiki": true, "pages": true }, + "enterprise-architecture-core": { + "description": "Enterprise Architecture Core — authoritative enterprise context maps, transformation decisions, and cross-context governance.", + "topics": [ + "enterprise-architecture", + "context-map", + "architecture-decisions", + "governance", + "transformation", + "domain-driven-design", + "contextualwisdomlab" + ], + "deepwiki": true, + "pages": true, + "homepage": "https://contextualwisdomlab.github.io/enterprise-architecture-core/" + }, + "EmbedRelay": { + "description": "EmbedRelay — governed embedding identity, compatibility, provenance, and migration for safe vector-model transitions.", + "topics": [ + "embeddings", + "vector-database", + "data-migration", + "model-migration", + "data-provenance", + "interoperability", + "contextualwisdomlab" + ], + "deepwiki": true, + "pages": true, + "homepage": "https://contextualwisdomlab.github.io/EmbedRelay/" + }, + "pg-llm-batch": { + "description": "pg-llm-batch — PostgreSQL-authoritative token counting, bounded JSONL batch assembly, and durable OpenAI-compatible batch lifecycle.", + "topics": [ + "postgresql", + "batch-processing", + "tokenization", + "openai-compatible", + "llm", + "jsonl", + "python", + "contextualwisdomlab" + ], + "deepwiki": true, + "pages": true, + "homepage": "https://contextualwisdomlab.github.io/pg-llm-batch/" + }, "ThreadWeave": { "description": "ThreadWeave — standards-grounded, deterministic email conversation threading for Python.", - "topics": ["email", "threading", "imap", "rfc5256", "python", "mail", "contextualwisdomlab"], + "topics": [ + "email", + "threading", + "imap", + "rfc5256", + "python", + "mail", + "contextualwisdomlab" + ], "deepwiki": true, "pages": true }, "RankWeave": { "description": "RankWeave — deterministic retrieval fusion, evaluation, statistical comparison, and auditable ranking workflows for Python.", - "topics": ["information-retrieval", "ranking", "retrieval", "reciprocal-rank-fusion", "trec", "python", "contextualwisdomlab"], + "topics": [ + "information-retrieval", + "ranking", + "retrieval", + "reciprocal-rank-fusion", + "trec", + "python", + "contextualwisdomlab" + ], "deepwiki": true, "pages": true }, "fast-mlsirm": { "description": "fast-mlsirm — high-performance psychometric modeling, calibration, and evaluation with a Rust numerical core.", - "topics": ["irt", "item-response-theory", "mlsirm", "psychometrics", "calibration", "measurement", "rust", "python", "simulation", "contextualwisdomlab"], + "topics": [ + "irt", + "item-response-theory", + "mlsirm", + "psychometrics", + "calibration", + "measurement", + "rust", + "python", + "simulation", + "contextualwisdomlab" + ], "deepwiki": true, "pages": true }, "EgressWeave": { "description": "EgressWeave — SSRF- and DNS-rebinding-safe outbound HTTP for Python.", - "topics": ["egress", "ssrf", "dns-rebinding", "http", "network-security", "httpx", "python", "contextualwisdomlab"], + "topics": [ + "egress", + "ssrf", + "dns-rebinding", + "http", + "network-security", + "httpx", + "python", + "contextualwisdomlab" + ], "deepwiki": true, "pages": true }, "psychometrics-commons": { "description": "Psychometrics Commons — governed psychometric assessment, longitudinal measurement, and consent-aware research workflows.", - "topics": ["psychometrics", "assessment", "measurement", "longitudinal", "research", "privacy", "rust", "contextualwisdomlab"], + "topics": [ + "psychometrics", + "assessment", + "measurement", + "longitudinal", + "research", + "privacy", + "rust", + "contextualwisdomlab" + ], "deepwiki": true, "pages": true }, "keyverse": { "description": "Keyverse — passwordless identity, federation, provisioning, account unification, and authorization services for ContextualWisdomLab.", - "topics": ["identity", "openid-connect", "oauth2", "scim", "keycloak", "python", "contextualwisdomlab"], + "topics": [ + "identity", + "openid-connect", + "oauth2", + "scim", + "keycloak", + "python", + "contextualwisdomlab" + ], "deepwiki": true, "pages": true }, "OriginWeave": { "description": "Let agents use the web without losing control. OriginWeave gives AI agents a Chromium-compatible web runtime with isolated sessions, typed actions, resource governance, and verifiable evidence.", - "topics": ["browser-automation", "ai-agents", "chromium", "security", "rust", "web", "contextualwisdomlab"], + "topics": [ + "browser-automation", + "ai-agents", + "chromium", + "security", + "rust", + "web", + "contextualwisdomlab" + ], "deepwiki": true, "pages": true }, "accounting-information-platform": { "description": "Accounting Information Platform — statutory accounting, journal posting, period control, reconciliation, and financial reporting authority for ContextualWisdomLab.", - "topics": ["accounting", "ledger", "journal", "reconciliation", "financial-reporting", "postgresql", "python", "contextualwisdomlab"], + "topics": [ + "accounting", + "ledger", + "journal", + "reconciliation", + "financial-reporting", + "postgresql", + "python", + "contextualwisdomlab" + ], "deepwiki": true, "pages": true }, "pg-erd-cloud": { "description": "PostgreSQL 스키마를 리버스 엔지니어링하고 ERD·DDL 공유 흐름으로 관리하는 클라우드 서비스.", - "topics": ["cloud", "database-schema", "ddl", "erd", "postgresql", "reverse-engineering", "saas", "python", "javascript", "contextualwisdomlab"], + "topics": [ + "cloud", + "database-schema", + "ddl", + "erd", + "postgresql", + "reverse-engineering", + "saas", + "python", + "javascript", + "contextualwisdomlab" + ], "deepwiki": true, "pages": true }, "clearfolio": { "description": "Clearfolio — secure document conversion, tenant-scoped viewing, and controlled artifact delivery.", - "topics": ["document-viewer", "document-conversion", "file-preview", "pdf", "java", "spring-boot", "javascript", "web-app", "contextualwisdomlab"], + "topics": [ + "document-viewer", + "document-conversion", + "file-preview", + "pdf", + "java", + "spring-boot", + "javascript", + "web-app", + "contextualwisdomlab" + ], "deepwiki": true, "pages": true }, "DiagramWeave": { "description": "DiagramWeave — a source-first, AI-assisted editor and tooling platform for PlantUML diagrams.", - "topics": ["diagram-editor", "plantuml", "developer-tools", "language-server", "javascript", "ai-assisted", "contextualwisdomlab"], + "topics": [ + "diagram-editor", + "plantuml", + "developer-tools", + "language-server", + "javascript", + "ai-assisted", + "contextualwisdomlab" + ], "deepwiki": true, "pages": true }, "semantic-data-portal": { "description": "Semantic Data Portal — governed discovery, graph traversal, and semantic search for enterprise data catalogs.", - "topics": ["data-catalog", "knowledge-graph", "ontology", "semantic-web", "semantic-search", "data-governance", "postgresql", "python", "contextualwisdomlab"], + "topics": [ + "data-catalog", + "knowledge-graph", + "ontology", + "semantic-web", + "semantic-search", + "data-governance", + "postgresql", + "python", + "contextualwisdomlab" + ], "deepwiki": true, "pages": true }, "contextual-orchestrator": { "description": "Contextual Orchestrator — an OpenAI-compatible control plane for model routing, delegation, verification, and multi-agent orchestration.", - "topics": ["enterprise-admin", "llm-orchestration", "model-orchestration", "model-routing", "ai-agents", "openai-compatible", "research", "python", "contextualwisdomlab"], + "topics": [ + "enterprise-admin", + "llm-orchestration", + "model-orchestration", + "model-routing", + "ai-agents", + "openai-compatible", + "research", + "python", + "contextualwisdomlab" + ], + "deepwiki": true, + "pages": true + }, + "noema": { + "description": "Noema — evidence-producing credential and maintenance control plane for governed GitHub automation.", + "topics": [ + "automation", + "code-review", + "control-plane", + "github-actions", + "github-app", + "llm", + "oidc", + "python", + "security", + "typescript", + "contextualwisdomlab" + ], "deepwiki": true, "pages": true }, "mhtml-etl-gateway": { "description": "Enterprise MHTML ingestion gateway that converts browser, SAP ALV, and Excel Web Archive exports into governed PostgreSQL data assets.", - "topics": ["mhtml", "etl", "data-ingestion", "sap", "postgresql", "data-governance", "python", "contextualwisdomlab"], + "topics": [ + "mhtml", + "etl", + "data-ingestion", + "sap", + "postgresql", + "data-governance", + "python", + "contextualwisdomlab" + ], "deepwiki": true, "pages": true }, "PolicyWeave": { "description": "PolicyWeave — local-first privacy-policy fact authoring, completeness review, and deterministic draft generation for web and app operators.", - "topics": ["privacy", "privacy-policy", "privacy-engineering", "policy-authoring", "local-first", "react", "typescript", "vite", "contextualwisdomlab"], + "topics": [ + "privacy", + "privacy-policy", + "privacy-engineering", + "policy-authoring", + "local-first", + "react", + "typescript", + "vite", + "contextualwisdomlab" + ], "deepwiki": true, "pages": true }, "supply-chain-control-plane": { "description": "Supply Chain Control Plane — evidence-backed supply-network dependency modeling and deterministic downstream disruption-impact analysis.", - "topics": ["supply-chain", "disruption-management", "dependency-graph", "provenance", "risk-analysis", "rust", "contextualwisdomlab"], + "topics": [ + "supply-chain", + "disruption-management", + "dependency-graph", + "provenance", + "risk-analysis", + "rust", + "contextualwisdomlab" + ], "deepwiki": true, "pages": true }, "learning-management-platform": { "description": "Learning Management Platform — enrollment, learning-journey, completion, and credential orchestration for employee and external learners.", - "topics": ["learning-management-system", "learning-platform", "enrollment", "completion", "credentialing", "rust", "postgresql", "contextualwisdomlab"], + "topics": [ + "learning-management-system", + "learning-platform", + "enrollment", + "completion", + "credentialing", + "rust", + "postgresql", + "contextualwisdomlab" + ], "deepwiki": true, "pages": true }, "learning-content-studio": { "description": "Learning Content Studio — evidence-bound LCMS for authoring, approving, releasing, and deterministically publishing reusable learning content.", - "topics": ["lcms", "learning-content", "content-authoring", "content-management", "accessibility", "scorm", "cmi5", "rust", "contextualwisdomlab"], + "topics": [ + "lcms", + "learning-content", + "content-authoring", + "content-management", + "accessibility", + "scorm", + "cmi5", + "rust", + "contextualwisdomlab" + ], "deepwiki": true, "pages": true }, "learning-record-store": { "description": "Authoritative xAPI learning-record persistence for the CWL Learning Platform.", - "topics": ["learning-record-store", "xapi", "cmi5", "learning-technology", "interoperability", "contextualwisdomlab"], + "topics": [ + "learning-record-store", + "xapi", + "cmi5", + "learning-technology", + "interoperability", + "contextualwisdomlab" + ], + "deepwiki": true, + "pages": true + }, + "bandscope": { + "description": "로컬 우선 리허설 앱: 곡을 섹션·역할·템포·연습 우선순위로 분석합니다.", + "topics": [ + "audio-analysis", + "local-first", + "music", + "practice-tool", + "python", + "rehearsal", + "contextualwisdomlab" + ], + "deepwiki": true, + "pages": true + }, + "saju-caldav": { + "description": "saju-caldav — personalized Four Pillars calendars published through CalDAV and iCalendar.", + "topics": [ + "caldav", + "fastapi", + "four-pillars", + "icalendar", + "saju", + "python", + "contextualwisdomlab" + ], + "deepwiki": true, + "pages": true + }, + "governance-risk-compliance": { + "description": "Governance, risk, control, evidence, and compliance workflows with auditable policy and standards mapping.", + "topics": [ + "governance", + "risk-management", + "compliance", + "grc", + "audit", + "python", + "contextualwisdomlab" + ], + "deepwiki": true, + "pages": true + }, + "metering-billing-platform": { + "description": "Metering & Billing Platform — provider-neutral usage attribution, metering, rating, entitlements, invoice intent, and reconciliation.", + "topics": [ + "metering", + "billing", + "usage-based-billing", + "entitlements", + "reconciliation", + "finops", + "python", + "postgresql", + "contextualwisdomlab" + ], + "deepwiki": true, + "pages": true + }, + "learning-interoperability-contracts": { + "description": "Learning Interoperability Contracts — versioned learning schemas, profiles, mappings, and conformance contracts.", + "topics": [ + "learning-technology", + "interoperability", + "xapi", + "cmi5", + "json-schema", + "contracts", + "contextualwisdomlab" + ], + "deepwiki": true, + "pages": true + }, + "litellm-patched-proxy": { + "description": "litellm-patched-proxy — hardened downstream LiteLLM proxy images with bounded production patches and supply-chain evidence.", + "topics": [ + "litellm", + "llm-proxy", + "container-image", + "supply-chain-security", + "vulnerability-scanning", + "sbom", + "python", + "contextualwisdomlab" + ], + "deepwiki": true, + "pages": true + }, + "pingora-gateway": { + "description": "Pingora Gateway — a shared Rust edge runtime for explicit, bounded reverse-proxy traffic and secure service ingress.", + "topics": [ + "reverse-proxy", + "edge-computing", + "pingora", + "rust", + "network-security", + "observability", + "contextualwisdomlab" + ], + "deepwiki": true, + "pages": true, + "homepage": "https://contextualwisdomlab.github.io/pingora-gateway/", + "pages_mode": "workflow" + }, + "global-hs-trade": { + "description": "Global HS Trade — evidence-aware collection, validation, and aggregation of rights-scoped company-by-HS trade observations.", + "topics": [ + "international-trade", + "hs-code", + "customs-data", + "trade-data", + "data-provenance", + "python", + "sqlite", + "contextualwisdomlab" + ], "deepwiki": true, "pages": true + }, + "LineageWeave": { + "description": "LineageWeave — evidence-bound reconstruction of branching lineage and project journeys from scattered records.", + "topics": [ + "data-lineage", + "lineage", + "knowledge-graph", + "provenance", + "semantic-web", + "evidence", + "python", + "typescript", + "contextualwisdomlab" + ], + "deepwiki": true, + "pages": true, + "homepage": "https://contextualwisdomlab.github.io/LineageWeave/", + "pages_mode": "workflow", + "pages_workflow": ".github/workflows/ontology-pages.yml" + }, + "j-planner": { + "description": "로그인 없이 일정, 지도 경로, 예약서류, 알람을 브라우저에 저장하는 개인용 여행 플래너 PWA.", + "topics": [ + "travel-planner", + "pwa", + "offline-first", + "leaflet", + "javascript", + "web-app", + "contextualwisdomlab" + ], + "deepwiki": true, + "pages": true, + "homepage": "https://contextualwisdomlab.github.io/j-planner/", + "pages_mode": "legacy-root" + }, + "disksage": { + "description": "DiskSage — local-first, evidence-backed disk inventory and reversible cleanup for Windows, Linux, and macOS.", + "topics": [ + "disk-cleanup", + "disk-usage", + "local-first", + "file-management", + "tauri", + "rust", + "svelte", + "cross-platform", + "contextualwisdomlab" + ], + "deepwiki": true, + "pages": true + }, + "Veilpick": { + "description": "Veilpick — ontology-guided, policy-governed web acquisition and evidence-backed structured extraction.", + "topics": [ + "web-acquisition", + "data-extraction", + "ontology", + "provenance", + "rust", + "browser-automation", + "contextualwisdomlab" + ], + "deepwiki": true, + "pages": true + }, + "BizPlanningWizard": { + "description": "A downstream of Business Plan Generator v2.0", + "topics": [ + "business-planning", + "business-plan", + "planning", + "productivity", + "contextualwisdomlab" + ], + "deepwiki": false, + "pages": false + }, + "litellm": { + "description": "The fastest, litest AI Gateway. Rust core with Python SDK. Call 100+ LLM APIs in OpenAI (or native) format with cost tracking, guardrails, load balancing, and logging [Bedrock, Azure, OpenAI, Anthropic, OpenAI, VertexAI, vLLM, Nvidia NIM]", + "topics": [ + "llm-gateway", + "openai-compatible", + "model-routing", + "load-balancing", + "guardrails", + "python", + "rust", + "contextualwisdomlab" + ], + "deepwiki": false, + "pages": false + }, + "opencode": { + "description": "The open source coding agent.", + "topics": [ + "coding-agent", + "ai-agents", + "developer-tools", + "terminal", + "contextualwisdomlab" + ], + "deepwiki": false, + "pages": false + }, + "orca": { + "description": "Orca is the ADE for working with a fleet of parallel agents. Run any coding agent with your own subscription. Available on desktop, mobile and remote runtime.", + "topics": [ + "ai-orchestration", + "coding-agents", + "git-worktrees", + "developer-tools", + "desktop-app", + "terminal", + "contextualwisdomlab" + ], + "deepwiki": false, + "pages": false + }, + "inkspan": { + "description": "Inkspan — modular Markdown and HTML authoring, collaboration, safe serialization, and deterministic Office document rendering.", + "topics": [ + "collaborative-editing", + "markdown-editor", + "office-documents", + "prosemirror", + "react", + "tiptap", + "typescript", + "wysiwyg-editor", + "contextualwisdomlab" + ], + "deepwiki": true, + "pages": true, + "homepage": "https://contextualwisdomlab.github.io/inkspan/" + }, + "appguardrail": { + "description": "AppGuardrail — security scanning, SARIF evidence, and guided remediation for AI-assisted applications.", + "topics": [ + "application-security", + "security-scanner", + "static-analysis", + "sarif", + "devsecops", + "ai-assisted-development", + "python", + "contextualwisdomlab" + ], + "deepwiki": true, + "pages": true, + "homepage": "https://contextualwisdomlab.github.io/appguardrail/" + }, + "ELUNVERA": { + "description": "ELUNVERA — evidence-centered enterprise CRM and relationship intelligence for governed commercial accounts, stakeholders, commitments, opportunities, and customer outcomes.", + "topics": [ + "crm", + "relationship-intelligence", + "customer-success", + "sales", + "account-management", + "data-governance", + "domain-driven-design", + "contextualwisdomlab" + ], + "deepwiki": true, + "pages": true, + "homepage": "https://contextualwisdomlab.github.io/ELUNVERA/" + }, + "four-pillars": { + "description": "Four Pillars — deterministic Korean Four Pillars calendar calculation with schema-validated, evidence-bounded report generation.", + "topics": [ + "four-pillars", + "korean-calendar", + "calendar-calculation", + "saju", + "fastapi", + "python", + "llm", + "pdf-generation", + "contextualwisdomlab" + ], + "deepwiki": true, + "pages": true, + "homepage": "https://contextualwisdomlab.github.io/four-pillars/" + }, + "quarantine-sandbox-runtime": { + "description": "Quarantine Sandbox Runtime — credential-free hostile-workload isolation and deterministic artifact-analysis evidence for security and Agent control planes.", + "topics": [ + "sandbox", + "container-security", + "malware-analysis", + "artifact-analysis", + "isolation", + "podman", + "rust", + "security", + "contextualwisdomlab" + ], + "deepwiki": true, + "pages": true, + "homepage": "https://contextualwisdomlab.github.io/quarantine-sandbox-runtime/" + }, + "TEPP": { + "description": "TEPP — multilingual, temporal, relational psychometrics with evidence-bound measurement, uncertainty, and event-time analysis.", + "topics": [ + "psychometrics", + "temporal-analysis", + "event-data", + "multilingual", + "longitudinal-data", + "measurement", + "data-provenance", + "rust", + "contextualwisdomlab" + ], + "deepwiki": true, + "pages": true, + "homepage": "https://contextualwisdomlab.github.io/TEPP/" } } } diff --git a/scripts/ci/reconcile_repository_metadata.py b/scripts/ci/reconcile_repository_metadata.py index 1570455818..7d6de9b879 100644 --- a/scripts/ci/reconcile_repository_metadata.py +++ b/scripts/ci/reconcile_repository_metadata.py @@ -1,7 +1,7 @@ """Reconcile public GitHub repository metadata from a reviewed desired-state manifest. The reconciler is intentionally narrow: it changes repository descriptions, -repository topics, and GitHub Pages settings. README content remains owned by +homepage URLs, repository topics, and GitHub Pages settings. README content remains owned by the target repository so badge/content changes can pass through that repository's normal review path. """ @@ -9,6 +9,7 @@ from __future__ import annotations import argparse +import ipaddress import json import os import re @@ -17,6 +18,7 @@ from pathlib import Path from typing import Any from urllib.error import URLError +from urllib.parse import urlsplit from urllib.request import HTTPRedirectHandler, Request, build_opener @@ -25,7 +27,9 @@ TOPIC_RE = re.compile(r"^[a-z0-9][a-z0-9-]{0,49}$") MAX_DESCRIPTION_CHARS = 350 PAGES_BASE_URL = f"https://{ORGANIZATION.casefold()}.github.io" -PAGES_MODES = {"legacy", "workflow"} +PAGES_MODES = {"legacy", "legacy-root", "workflow"} +DEFAULT_PAGES_WORKFLOW = ".github/workflows/pages.yml" +PAGES_WORKFLOW_RE = re.compile(r"^\.github/workflows/[A-Za-z0-9_.-]+\.ya?ml$") class ManifestError(ValueError): @@ -58,10 +62,10 @@ def _validate_repository(name: str, raw: Any) -> dict[str, Any]: raise ManifestError("repository names must preserve exact GitHub-safe casing") item = _require_exact_dict(raw, field=f"repositories.{name}") required = {"description", "topics", "deepwiki", "pages"} - allowed = required | {"pages_mode"} + allowed = required | {"homepage", "pages_mode", "pages_workflow"} if not required.issubset(item) or not set(item).issubset(allowed): raise ManifestError( - f"repositories.{name} must contain exactly {sorted(required)} plus optional pages_mode" + f"repositories.{name} must contain exactly {sorted(required)} plus optional homepage/pages_mode/pages_workflow" ) description = item["description"] @@ -92,6 +96,31 @@ def _validate_repository(name: str, raw: Any) -> dict[str, Any]: if len(set(topics)) != len(topics): raise ManifestError(f"repositories.{name}.topics contains duplicates") + if "homepage" in item: + homepage = item["homepage"] + if homepage is not None: + if type(homepage) is not str or homepage != homepage.strip(): + raise ManifestError(f"repositories.{name}.homepage is invalid") + parsed = urlsplit(homepage) + hostname = parsed.hostname + normalized_hostname = hostname.rstrip(".").casefold() if hostname else "" + try: + internal_address = bool(normalized_hostname) and not ipaddress.ip_address( + normalized_hostname + ).is_global + except ValueError: + internal_address = False + if ( + parsed.scheme != "https" + or not normalized_hostname + or parsed.username is not None + or parsed.password is not None + or normalized_hostname == "localhost" + or normalized_hostname.endswith((".internal", ".local", ".localhost")) + or internal_address + ): + raise ManifestError(f"repositories.{name}.homepage is invalid") + if type(item["deepwiki"]) is not bool or type(item["pages"]) is not bool: raise ManifestError( f"repositories.{name} deepwiki/pages flags must be booleans" @@ -105,6 +134,15 @@ def _validate_repository(name: str, raw: Any) -> dict[str, Any]: raise ManifestError( f"repositories.{name}.pages_mode is only valid when Pages is enabled" ) + pages_workflow = item.get("pages_workflow", DEFAULT_PAGES_WORKFLOW) + if "pages_workflow" in item and ( + pages_mode != "workflow" + or type(pages_workflow) is not str + or not PAGES_WORKFLOW_RE.fullmatch(pages_workflow) + ): + raise ManifestError( + f"repositories.{name}.pages_workflow requires a safe Actions workflow path" + ) validated = { "description": description, @@ -112,8 +150,12 @@ def _validate_repository(name: str, raw: Any) -> dict[str, Any]: "deepwiki": item["deepwiki"], "pages": item["pages"], } + if "homepage" in item: + validated["homepage"] = item["homepage"] if "pages_mode" in item: validated["pages_mode"] = pages_mode + if "pages_workflow" in item: + validated["pages_workflow"] = pages_workflow return validated @@ -202,15 +244,17 @@ def _pages_configuration(repository: str) -> dict[str, Any]: return _require_exact_dict(payload, field=f"Pages configuration for {repository}") -def _pages_configuration_matches(current: dict[str, Any], default_branch: str) -> bool: - """Return whether Pages already serves the desired legacy /docs source.""" +def _pages_configuration_matches( + current: dict[str, Any], default_branch: str, source_path: str = "/docs" +) -> bool: + """Return whether Pages already serves the desired legacy source path.""" source = current.get("source") if type(source) is not dict: return False return ( source.get("branch") == default_branch - and source.get("path") == "/docs" + and source.get("path") == source_path and current.get("build_type") in (None, "legacy") ) @@ -279,12 +323,18 @@ def _docs_index_exists(repository: str, default_branch: str) -> bool: return _repository_file_exists(repository, default_branch, "docs/index.md") -def _workflow_pages_definition_exists(repository: str, default_branch: str) -> bool: - """Return whether the standard reviewed Pages workflow exists on the default branch.""" +def _root_index_exists(repository: str, default_branch: str) -> bool: + """Return whether the reviewed default branch contains root index.html.""" - return _repository_file_exists( - repository, default_branch, ".github/workflows/pages.yml" - ) + return _repository_file_exists(repository, default_branch, "index.html") + + +def _workflow_pages_definition_exists( + repository: str, default_branch: str, workflow_path: str = DEFAULT_PAGES_WORKFLOW +) -> bool: + """Return whether the reviewed Pages workflow exists on the default branch.""" + + return _repository_file_exists(repository, default_branch, workflow_path) def _deepwiki_badge_linked(readme: str, repository: str) -> bool: @@ -336,9 +386,18 @@ def _pages_precondition(repository: str, default_branch: str, desired: dict[str, return pages_mode = desired.get("pages_mode", "legacy") if pages_mode == "workflow": - if not _workflow_pages_definition_exists(repository, default_branch): + workflow_path = desired.get("pages_workflow", DEFAULT_PAGES_WORKFLOW) + if not _workflow_pages_definition_exists( + repository, default_branch, workflow_path + ): raise RuntimeError( - f"workflow Pages requested for {repository} but .github/workflows/pages.yml is not on {default_branch}" + f"workflow Pages requested for {repository} but {workflow_path} is not on {default_branch}" + ) + return + if pages_mode == "legacy-root": + if not _root_index_exists(repository, default_branch): + raise RuntimeError( + f"root Pages requested for {repository} but index.html is not on {default_branch}" ) return if not _docs_index_exists(repository, default_branch): @@ -385,11 +444,18 @@ def reconcile_repository(repository: str, desired: dict[str, Any]) -> None: _pages_precondition(repository, default_branch, desired) _workflow_pages_live_precondition(repository, desired) + repository_patch = {} if repository_payload.get("description") != desired["description"]: + repository_patch["description"] = desired["description"] + if "homepage" in desired and (repository_payload.get("homepage") or None) != desired[ + "homepage" + ]: + repository_patch["homepage"] = desired["homepage"] + if repository_patch: _gh_api( "PATCH", f"repos/{ORGANIZATION}/{repository}", - body={"description": desired["description"]}, + body=repository_patch, ) current_topics = json.loads( @@ -408,9 +474,10 @@ def reconcile_repository(repository: str, desired: dict[str, Any]) -> None: pages_exists = _pages_exists(repository) if desired["pages"]: + source_path = "/" if pages_mode == "legacy-root" else "/docs" pages_body = { "build_type": "legacy", - "source": {"branch": default_branch, "path": "/docs"}, + "source": {"branch": default_branch, "path": source_path}, } if not pages_exists: _gh_api( @@ -419,7 +486,7 @@ def reconcile_repository(repository: str, desired: dict[str, Any]) -> None: body=pages_body, ) elif not _pages_configuration_matches( - _pages_configuration(repository), default_branch + _pages_configuration(repository), default_branch, source_path ): _gh_api( "PUT", @@ -441,6 +508,10 @@ def verify_repository(repository: str, desired: dict[str, Any]) -> None: raise RuntimeError(f"default branch could not be resolved for {repository}") if repository_payload.get("description") != desired["description"]: raise RuntimeError(f"description did not converge for {repository}") + if "homepage" in desired and (repository_payload.get("homepage") or None) != desired[ + "homepage" + ]: + raise RuntimeError(f"homepage did not converge for {repository}") current_topics = json.loads( _gh_api("GET", f"repos/{ORGANIZATION}/{repository}/topics") @@ -454,10 +525,16 @@ def verify_repository(repository: str, desired: dict[str, Any]) -> None: if desired["pages"]: pages_mode = desired.get("pages_mode", "legacy") if pages_mode == "workflow": - if not _workflow_pages_definition_exists(repository, default_branch): + workflow_path = desired.get("pages_workflow", DEFAULT_PAGES_WORKFLOW) + if not _workflow_pages_definition_exists( + repository, default_branch, workflow_path + ): raise RuntimeError( f"Pages workflow source did not converge for {repository}" ) + elif pages_mode == "legacy-root": + if not _root_index_exists(repository, default_branch): + raise RuntimeError(f"Pages root source did not converge for {repository}") elif not _docs_index_exists(repository, default_branch): raise RuntimeError(f"Pages source did not converge for {repository}") @@ -472,8 +549,14 @@ def verify_repository(repository: str, desired: dict[str, Any]) -> None: raise RuntimeError( f"GitHub Pages deployment mode did not converge for {repository}" ) - elif not _pages_configuration_matches(current_pages, default_branch): - raise RuntimeError(f"GitHub Pages configuration did not converge for {repository}") + else: + source_path = "/" if pages_mode == "legacy-root" else "/docs" + if not _pages_configuration_matches( + current_pages, default_branch, source_path + ): + raise RuntimeError( + f"GitHub Pages configuration did not converge for {repository}" + ) _pages_publication_ready(repository, current_pages) elif pages_exists: raise RuntimeError(f"GitHub Pages remained published for {repository}") diff --git a/tests/test_repository_metadata_reconciliation.py b/tests/test_repository_metadata_reconciliation.py index 2bfc9d1386..495ff37b0e 100644 --- a/tests/test_repository_metadata_reconciliation.py +++ b/tests/test_repository_metadata_reconciliation.py @@ -62,10 +62,19 @@ def test_metadata_manifest_declares_exact_casing_and_public_surfaces() -> None: payload = json.loads(MANIFEST.read_text(encoding="utf-8")) repositories = payload["repositories"] - expected = { + public_surfaces = { "CalendarWeave": ("calendar", "icalendar"), "ConceptWeave": ("semantic-model", "ontology"), "context-graph-contracts": ("interoperability", "cloudevents"), + "enterprise-architecture-core": ("enterprise-architecture", "context-map"), + "EmbedRelay": ("embeddings", "data-migration"), + "pg-llm-batch": ("postgresql", "batch-processing"), + "inkspan": ("markdown-editor", "collaborative-editing"), + "appguardrail": ("application-security", "sarif"), + "ELUNVERA": ("crm", "relationship-intelligence"), + "four-pillars": ("four-pillars", "korean-calendar"), + "quarantine-sandbox-runtime": ("sandbox", "container-security"), + "TEPP": ("psychometrics", "temporal-analysis"), "ThreadWeave": ("rfc5256", "python"), "RankWeave": ("information-retrieval", "trec"), "fast-mlsirm": ("psychometrics", "rust"), @@ -79,19 +88,85 @@ def test_metadata_manifest_declares_exact_casing_and_public_surfaces() -> None: "DiagramWeave": ("diagram-editor", "plantuml"), "semantic-data-portal": ("data-catalog", "semantic-search"), "contextual-orchestrator": ("llm-orchestration", "model-routing"), + "noema": ("control-plane", "oidc"), "mhtml-etl-gateway": ("mhtml", "etl"), "PolicyWeave": ("privacy-policy", "typescript"), "supply-chain-control-plane": ("supply-chain", "rust"), "learning-management-platform": ("learning-management-system", "rust"), "learning-content-studio": ("lcms", "content-authoring"), "learning-record-store": ("learning-record-store", "xapi"), + "bandscope": ("audio-analysis", "rehearsal"), + "saju-caldav": ("caldav", "four-pillars"), + "governance-risk-compliance": ("governance", "grc"), + "metering-billing-platform": ("metering", "billing"), + "learning-interoperability-contracts": ("xapi", "json-schema"), + "litellm-patched-proxy": ("llm-proxy", "supply-chain-security"), + "pingora-gateway": ("reverse-proxy", "rust"), + "global-hs-trade": ("international-trade", "hs-code"), + "LineageWeave": ("data-lineage", "knowledge-graph"), + "j-planner": ("travel-planner", "pwa"), + "disksage": ("disk-cleanup", "rust"), + "Veilpick": ("web-acquisition", "rust"), } - assert set(repositories) == set(expected) - for repository, required_topics in expected.items(): + topics_only = { + "BizPlanningWizard": ("business-planning", "productivity"), + "litellm": ("llm-gateway", "openai-compatible"), + "opencode": ("coding-agent", "developer-tools"), + "orca": ("ai-orchestration", "git-worktrees"), + } + assert set(repositories) == set(public_surfaces) | set(topics_only) + for repository, required_topics in public_surfaces.items(): state = repositories[repository] assert state["deepwiki"] is True assert state["pages"] is True assert all(topic in state["topics"] for topic in required_topics) + for repository, required_topics in topics_only.items(): + state = repositories[repository] + assert state["deepwiki"] is False + assert state["pages"] is False + assert all(topic in state["topics"] for topic in required_topics) + assert repositories["j-planner"]["pages_mode"] == "legacy-root" + assert repositories["j-planner"]["homepage"] == ( + "https://contextualwisdomlab.github.io/j-planner/" + ) + assert repositories["LineageWeave"]["pages_mode"] == "workflow" + assert repositories["LineageWeave"]["pages_workflow"] == ( + ".github/workflows/ontology-pages.yml" + ) + assert repositories["LineageWeave"]["homepage"] == ( + "https://contextualwisdomlab.github.io/LineageWeave/" + ) + assert repositories["pingora-gateway"]["pages_mode"] == "workflow" + assert repositories["pingora-gateway"]["homepage"] == ( + "https://contextualwisdomlab.github.io/pingora-gateway/" + ) + assert repositories["enterprise-architecture-core"]["homepage"] == ( + "https://contextualwisdomlab.github.io/enterprise-architecture-core/" + ) + assert repositories["EmbedRelay"]["homepage"] == ( + "https://contextualwisdomlab.github.io/EmbedRelay/" + ) + assert repositories["pg-llm-batch"]["homepage"] == ( + "https://contextualwisdomlab.github.io/pg-llm-batch/" + ) + assert repositories["inkspan"]["homepage"] == ( + "https://contextualwisdomlab.github.io/inkspan/" + ) + assert repositories["appguardrail"]["homepage"] == ( + "https://contextualwisdomlab.github.io/appguardrail/" + ) + assert repositories["ELUNVERA"]["homepage"] == ( + "https://contextualwisdomlab.github.io/ELUNVERA/" + ) + assert repositories["four-pillars"]["homepage"] == ( + "https://contextualwisdomlab.github.io/four-pillars/" + ) + assert repositories["quarantine-sandbox-runtime"]["homepage"] == ( + "https://contextualwisdomlab.github.io/quarantine-sandbox-runtime/" + ) + assert repositories["TEPP"]["homepage"] == ( + "https://contextualwisdomlab.github.io/TEPP/" + ) def test_require_exact_dict_and_repository_validation() -> None: @@ -103,7 +178,7 @@ def test_require_exact_dict_and_repository_validation() -> None: valid = desired() assert RECONCILER._validate_repository("Repo", valid) == valid - for name in [1, "bad name"]: + for name in [1, "bad name", "Repo..Name", "Repo."]: with pytest.raises(RECONCILER.ManifestError, match="exact GitHub-safe casing"): RECONCILER._validate_repository(name, valid) with pytest.raises(RECONCILER.ManifestError, match="contain exactly"): @@ -132,6 +207,27 @@ def test_require_exact_dict_and_repository_validation() -> None: with pytest.raises(RECONCILER.ManifestError): RECONCILER._validate_repository("Repo", {**valid, field: value}) + assert RECONCILER._validate_repository( + "Repo", desired(homepage=None) + )["homepage"] is None + assert RECONCILER._validate_repository( + "Repo", desired(homepage="https://example.com/docs") + )["homepage"] == "https://example.com/docs" + for homepage in [ + " https://example.com", + "http://example.com", + "not-a-url", + "https://localhost/docs", + "https://localhost./docs", + "https://127.0.0.1/docs", + "https://service.internal/docs", + "https://service.internal./docs", + ]: + with pytest.raises(RECONCILER.ManifestError, match="homepage"): + RECONCILER._validate_repository( + "Repo", desired(homepage=homepage) + ) + def test_load_manifest_contracts(tmp_path) -> None: """Manifest root schema, ownership, and non-empty fleet scope are enforced.""" @@ -378,6 +474,88 @@ def test_reconcile_preconditions(monkeypatch) -> None: RECONCILER.reconcile_repository("Repo", desired()) + +def test_custom_workflow_pages_contract(monkeypatch) -> None: + """Actions Pages may name one reviewed repository-owned workflow.""" + + state = RECONCILER._validate_repository( + "Repo", + desired( + pages=True, + pages_mode="workflow", + pages_workflow=".github/workflows/ontology-pages.yml", + ), + ) + seen = [] + monkeypatch.setattr( + RECONCILER, + "_repository_file_exists", + lambda repository, branch, path: seen.append(path) or True, + ) + RECONCILER._pages_precondition("Repo", "main", state) + assert seen == [".github/workflows/ontology-pages.yml"] + + for path in [ + "ontology-pages.yml", + ".github/workflows/../pages.yml", + ".github/workflows/pages.yaml.txt", + ]: + with pytest.raises(RECONCILER.ManifestError, match="pages_workflow"): + RECONCILER._validate_repository( + "Repo", + desired( + pages=True, + pages_mode="workflow", + pages_workflow=path, + ), + ) + + with pytest.raises(RECONCILER.ManifestError, match="pages_workflow"): + RECONCILER._validate_repository( + "Repo", + desired(pages=True, pages_workflow=".github/workflows/pages.yml"), + ) + + +def test_legacy_root_pages_contract(monkeypatch) -> None: + """Legacy root Pages preserve the / source and require root index.html.""" + + state = RECONCILER._validate_repository( + "Repo", desired(pages=True, pages_mode="legacy-root") + ) + calls = [] + + def gh_api(method, endpoint, **kwargs): + calls.append((method, endpoint, kwargs)) + if endpoint.endswith("/topics"): + return json.dumps({"names": ["python"]}) + if endpoint.endswith("/pages"): + return json.dumps( + {"build_type": "legacy", "source": {"branch": "main", "path": "/docs"}} + ) + if method == "GET": + return json.dumps( + {"default_branch": "main", "description": "Useful product."} + ) + return "" + + monkeypatch.setattr(RECONCILER, "_gh_api", gh_api) + monkeypatch.setattr(RECONCILER, "_deepwiki_badge_exists", lambda *args: False) + monkeypatch.setattr( + RECONCILER, + "_repository_file_exists", + lambda repository, branch, path: path == "index.html", + ) + monkeypatch.setattr(RECONCILER, "_pages_exists", lambda *args: True) + RECONCILER.reconcile_repository("Repo", state) + + pages_updates = [ + call for call in calls if call[0] == "PUT" and call[1].endswith("/pages") + ] + assert [call[2]["body"] for call in pages_updates] == [ + {"build_type": "legacy", "source": {"branch": "main", "path": "/"}} + ] + def test_reconcile_mutation_matrix(monkeypatch) -> None: """Descriptions, topics, Pages create/update/disable all reconcile.""" @@ -406,12 +584,38 @@ def gh_api(method, endpoint, **kwargs): topics=["new"], deepwiki=True, pages=True, + homepage="https://example.com/docs", ), ) - assert any(call[0] == "PATCH" for call in calls) + repository_patches = [ + call for call in calls if call[0] == "PATCH" and call[1].endswith("/Repo") + ] + assert [call[2]["body"] for call in repository_patches] == [ + { + "description": "new", + "homepage": "https://example.com/docs", + } + ] assert any(call[0] == "PUT" and call[1].endswith("/topics") for call in calls) assert any(call[0] == "POST" and call[1].endswith("/pages") for call in calls) + calls.clear() + RECONCILER.reconcile_repository( + "Repo", + desired( + description="old", + topics=["old"], + deepwiki=True, + homepage="https://example.com/docs", + ), + ) + repository_patches = [ + call for call in calls if call[0] == "PATCH" and call[1].endswith("/Repo") + ] + assert [call[2]["body"] for call in repository_patches] == [ + {"homepage": "https://example.com/docs"} + ] + calls.clear() monkeypatch.setattr(RECONCILER, "_pages_exists", lambda *args: True) RECONCILER.reconcile_repository( @@ -450,7 +654,7 @@ def gh_api(method, endpoint, **kwargs): monkeypatch.setattr(RECONCILER, "_gh_api", gh_api) monkeypatch.setattr(RECONCILER, "_deepwiki_badge_exists", lambda *args: False) monkeypatch.setattr(RECONCILER, "_pages_exists", lambda *args: False) - RECONCILER.reconcile_repository("Repo", desired()) + RECONCILER.reconcile_repository("Repo", desired(homepage=None)) assert [call[0] for call in calls] == ["GET", "GET"] calls.clear() @@ -461,6 +665,29 @@ def gh_api(method, endpoint, **kwargs): assert [call[0] for call in calls] == ["GET", "GET", "GET"] +def test_verify_rejects_homepage_drift(monkeypatch) -> None: + """Verification fails closed when managed homepage state drifts.""" + + def gh_api(method, endpoint, **kwargs): + if endpoint.endswith("/topics"): + return json.dumps({"names": ["python"]}) + return json.dumps( + { + "default_branch": "main", + "description": "Useful product.", + "homepage": "https://wrong.example.com", + } + ) + + monkeypatch.setattr(RECONCILER, "_gh_api", gh_api) + monkeypatch.setattr(RECONCILER, "_deepwiki_badge_exists", lambda *args: False) + monkeypatch.setattr(RECONCILER, "_pages_exists", lambda *args: False) + with pytest.raises(RuntimeError, match="homepage did not converge"): + RECONCILER.verify_repository( + "Repo", desired(homepage="https://example.com/docs") + ) + + def test_parse_args(monkeypatch, tmp_path) -> None: """CLI supports validation and narrow repository selection.""" @@ -574,4 +801,4 @@ def test_module_main_guard(monkeypatch, tmp_path) -> None: ) with pytest.raises(SystemExit) as exc: runpy.run_path(str(SCRIPT), run_name="__main__") - assert exc.value.code == 0 + assert exc.value.code == 0 \ No newline at end of file