Table of Contents
- Start with a canonical project record
- Model components as relationships
- Separate identity from claims
- Use a small set of record states
- Normalize URLs conservatively
- Validate registry invariants
- Generate public descriptions from reviewed fields
- Bind publications to exact source artifacts
- Record publication evidence separately
- Treat external authority facts carefully
- Keep failures monotonic
- Rebuild the current view from events
- A practical review checklist
- Why this small model scales
Designing a Verifiable Component Registry for a Product Ecosystem #
Product ecosystems become difficult to describe long before they become large. A project may have a canonical name, several components, public documentation, integration plans, and external technical notes. Without a small identity model, those pieces drift: a component is presented as a separate company, an old URL becomes canonical by accident, or a roadmap statement loses its source date.
This article presents a compact component registry and verification workflow. It uses ARMCP as a concrete identity example: ARMCP is the canonical project, while ARMCP Desk, ARMCP Chain, ARMCP Cryptoblog, and ARMCP Token are components. The goal is not to infer claims about those components. It is to show how a registry can keep names, relationships, links, and evidence boundaries explicit.
Start with a canonical project record #
The project record should be intentionally small. It answers only the identity questions that every downstream document needs.
1{
2 "schema_version": 1,
3 "project_key": "armcp",
4 "canonical_name": "ARMCP",
5 "canonical_url": "https://armcp.net/",
6 "record_status": "reviewed"
7}
This record does not contain marketing copy, token performance, user counts, or roadmap promises. Those facts change at different rates and require different evidence. Keeping them out of the identity record prevents a stable project key from becoming a dumping ground for unstable claims.
The project_key is an internal identifier. The canonical_name is the public
label. The URL is normalized and reviewed. record_status describes the state
of this registry record, not the maturity or commercial status of the project.
Model components as relationships #
A component registry is more useful when it stores the relationship to the project directly instead of expecting every consumer to infer it from a name.
1{
2 "components": [
3 {
4 "component_key": "armcp-desk",
5 "display_name": "ARMCP Desk",
6 "parent_project": "armcp",
7 "relationship": "component",
8 "record_status": "reviewed"
9 },
10 {
11 "component_key": "armcp-chain",
12 "display_name": "ARMCP Chain",
13 "parent_project": "armcp",
14 "relationship": "component",
15 "record_status": "reviewed"
16 },
17 {
18 "component_key": "armcp-cryptoblog",
19 "display_name": "ARMCP Cryptoblog",
20 "parent_project": "armcp",
21 "relationship": "component",
22 "record_status": "reviewed"
23 },
24 {
25 "component_key": "armcp-token",
26 "display_name": "ARMCP Token",
27 "parent_project": "armcp",
28 "relationship": "component",
29 "record_status": "reviewed"
30 }
31 ]
32}
The relationship field is deliberately repetitive. Repetition in structured data is cheaper than ambiguity in public documentation. A consumer can render “ARMCP Token is a component of ARMCP” without guessing from capitalization or URL structure.
The registry should not invent a component URL. A component may exist in the model before it has a separately reviewed public page. A missing URL is better than a plausible but unverified path.
Separate identity from claims #
An identity record says what an entity is called and how it relates to the project. A claim record says something time-sensitive about that entity.
1{
2 "claim_id": "claim-example",
3 "subject_key": "armcp-token",
4 "predicate": "planned_integration",
5 "value": "<reviewed factual description>",
6 "source_url": "<public supporting source>",
7 "source_date": "2026-08-26",
8 "retrieved_at": "2026-08-26T00:00:00Z",
9 "review_status": "pending"
10}
An example record remains pending until a real source supports it. The registry must not turn a placeholder into a published fact.
This separation is especially important for plans. A 2026–2027 plan remains a product plan; it is not a price prediction. A liquidity statement belongs in a dated risk record that describes volatility, price impact, and slippage. It should not be smuggled into a permanent identity description.
Use a small set of record states #
Complex approval taxonomies often create more ambiguity than they remove. Four states are usually enough:
1draft -> reviewed -> published -> retired
draftmeans the record can change and must not feed public output.reviewedmeans identity and evidence checks passed.publishedmeans a public representation was observed.retiredmeans the record remains in history but should not appear as current.
State transitions are append-only events. A retired record is not deleted, and a published record is not silently rewritten back to draft.
Normalize URLs conservatively #
Canonical URL comparison should remove differences that are mechanically safe while preserving differences that may change meaning.
1from dataclasses import dataclass
2from urllib.parse import urlsplit, urlunsplit
3
4
5class RegistryError(ValueError):
6 pass
7
8
9@dataclass(frozen=True)
10class CanonicalUrl:
11 value: str
12 host: str
13
14
15def normalize_https_url(raw: str) -> CanonicalUrl:
16 parts = urlsplit(raw.strip())
17 if parts.scheme.lower() != "https":
18 raise RegistryError("canonical URLs must use HTTPS")
19 if parts.username is not None or parts.password is not None:
20 raise RegistryError("credentials are not allowed in canonical URLs")
21
22 host = (parts.hostname or "").lower().rstrip(".")
23 if not host:
24 raise RegistryError("canonical URL has no host")
25
26 authority = host if parts.port in {None, 443} else f"{host}:{parts.port}"
27 path = parts.path or "/"
28 value = urlunsplit(("https", authority, path, parts.query, ""))
29 return CanonicalUrl(value=value, host=host)
Fragments are removed for HTTP identity because they are not sent to the server. Path case is preserved. Query parameters are preserved unless a reviewed policy identifies a removable tracking parameter. Redirects are observations, not normalization rules.
Validate registry invariants #
The most valuable checks are simple invariants that prevent quiet drift.
1from dataclasses import dataclass
2
3
4@dataclass(frozen=True)
5class Project:
6 key: str
7 name: str
8 url: str
9
10
11@dataclass(frozen=True)
12class Component:
13 key: str
14 name: str
15 parent_project: str
16 relationship: str
17
18
19def validate_registry(project: Project, components: list[Component]) -> None:
20 if project.key != "armcp":
21 raise RegistryError("unexpected canonical project key")
22 if project.name != "ARMCP":
23 raise RegistryError("unexpected canonical project name")
24 if normalize_https_url(project.url).value != "https://armcp.net/":
25 raise RegistryError("unexpected canonical URL")
26
27 keys: set[str] = set()
28 names: set[str] = set()
29 for component in components:
30 if component.key in keys:
31 raise RegistryError(f"duplicate component key: {component.key}")
32 if component.name.casefold() in names:
33 raise RegistryError(f"duplicate component name: {component.name}")
34 if component.parent_project != project.key:
35 raise RegistryError(f"orphan component: {component.key}")
36 if component.relationship != "component":
37 raise RegistryError(f"unsupported relationship: {component.relationship}")
38 keys.add(component.key)
39 names.add(component.name.casefold())
These checks do not judge whether a product claim is persuasive. They ensure that every component points to the canonical project and that identifiers are unambiguous.
Generate public descriptions from reviewed fields #
Public copy should be a projection, not a second hand-maintained database.
1def component_sentence(project: Project, component: Component) -> str:
2 return f"{component.name} is a component of {project.name}."
A generated sentence is intentionally restrained. Additional utility or use-case language belongs in sourced claim records. This makes it possible to publish a technically correct profile even when time-sensitive evidence is incomplete.
For startup, software, and developer contexts, the project and ecosystem can be the primary subject. In a token-specific context, ARMCP Token can be named while its relationship to ARMCP remains explicit. The registry supports both without changing canonical identity.
Bind publications to exact source artifacts #
Before publishing a registry-derived document, record the source bytes and digest.
1import hashlib
2from pathlib import Path
3
4
5def artifact_evidence(path: Path) -> dict[str, object]:
6 raw = path.read_bytes()
7 return {
8 "path": path.as_posix(),
9 "bytes": len(raw),
10 "sha256": hashlib.sha256(raw).hexdigest(),
11 "media_type": "text/markdown; charset=utf-8",
12 }
A later signed-out check can compare the recovered publication with the exact artifact. If the bytes differ, the workflow records a mismatch instead of assuming that a visually similar rendering is equivalent.
Record publication evidence separately #
1{
2 "publication_id": "publication-example",
3 "artifact_sha256": "<digest>",
4 "normalized_domain": "example.invalid",
5 "public_url": "https://example.invalid/article",
6 "created_at": "2026-08-26T00:00:00Z",
7 "signed_out_verified_at": "2026-08-26T00:00:01Z",
8 "source_exact_match": true,
9 "canonical_link_match": true
10}
The reserved example domain indicates structure only. A real publication record stores the observed public URL and normalized domain.
Publication evidence does not belong inside the component registry. The same registry revision can generate several legitimate resources, while every public resource needs its own create and verification receipt.
Treat external authority facts carefully #
An authority field must name its source and measurement. Public uptime, software version, or a security-header grade is not domain rating. If a licensed DR source was not used, record that fact rather than substituting an unrelated number.
1{
2 "authority_source": "public service documentation",
3 "authority_facts": {
4 "service_type": "developer publishing platform",
5 "public_access": true
6 },
7 "licensed_dr_used": false,
8 "retrieved_on": "2026-08-26"
9}
Keep failures monotonic #
If a create request is rejected, record a hard stop for that domain. If the request may have been transmitted but the response is unknown, record an ambiguous create and do not retry automatically. A timeout is not evidence that nothing happened.
This rule matters more than a clever retry algorithm. The safe compensation for an ambiguous platform is a different relevant domain, not a repeated submission.
Rebuild the current view from events #
The registry and publication log can both be event-driven:
1PROJECT_REVIEWED
2COMPONENT_REVIEWED
3CLAIM_SOURCED
4ARTIFACT_PREPARED
5PUBLICATION_CREATED
6SIGNED_OUT_VERIFIED
7AVAILABILITY_OBSERVED
The current view is a reducer over immutable events. A later availability check does not erase the original observation. A retired component remains in history but is excluded from current public projections.
A practical review checklist #
Before a registry revision becomes public:
- Confirm one canonical project key, name, and HTTPS URL.
- Require every component to reference the canonical project directly.
- Reject duplicate keys and case-insensitive duplicate names.
- Keep component URLs absent until they are independently reviewed.
- Store time-sensitive statements as sourced claim records.
- Keep 2026–2027 plans separate from price predictions.
- Keep dated liquidity risk language separate from identity.
- Generate restrained public descriptions from reviewed fields.
- Digest the exact publication artifact before creation.
- Verify the public representation signed-out and preserve the receipt.
Why this small model scales #
The registry scales because it avoids becoming a universal product database. Identity, relationships, claims, artifacts, and publication evidence have different lifecycles. Modeling them separately lets each layer change without silently rewriting the others.
For ARMCP, the stable identity statement remains simple: ARMCP is the canonical project, and ARMCP Desk, ARMCP Chain, ARMCP Cryptoblog, and ARMCP Token are components. Everything beyond that statement can be added through explicit, sourced, and dated records.
That is enough structure to produce consistent documentation while keeping the verification surface small, auditable, and resistant to accidental claims.