Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
191 changes: 191 additions & 0 deletions Discovery/adr_discovery/catalog/catalog.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
{
"version": "2026.08.22",
"entries": [
{
"id": "claude-code", "name": "Claude Code", "vendor": "Anthropic", "kind": "cli_agent",
"fingerprints": {"binaries": ["claude"], "packages": ["npm:@anthropic-ai/claude-code"],
"state_dirs": ["~/.claude"]},
"proofs": {"provenance": ["npm:@anthropic-ai/claude-code", "brew:claude"],
"version_probe": ["--version"], "version_shape": "^\\d+\\.\\d+\\.\\d+"},
"risk_factors": ["executes_shell"]
},
{
"id": "gemini-cli", "name": "Gemini CLI", "vendor": "Google", "kind": "cli_agent",
"fingerprints": {"binaries": ["gemini"], "packages": ["npm:@google/gemini-cli"],
"state_dirs": ["~/.gemini"]},
"proofs": {"provenance": ["npm:@google/gemini-cli"],
"version_probe": ["--version"], "version_shape": "^\\d+\\.\\d+\\.\\d+$"},
"risk_factors": ["executes_shell"]
},
{
"id": "codex-cli", "name": "Codex CLI", "vendor": "OpenAI", "kind": "cli_agent",
"fingerprints": {"binaries": ["codex"], "packages": ["npm:@openai/codex"],
"state_dirs": ["~/.codex"]},
"proofs": {"provenance": ["npm:@openai/codex"],
"version_probe": ["--version"], "version_shape": "^codex-cli \\d+\\.\\d+\\.\\d+|^\\d+\\.\\d+\\.\\d+"},
"risk_factors": ["executes_shell"]
},
{
"id": "aider", "name": "Aider", "vendor": "Aider AI", "kind": "cli_agent",
"fingerprints": {"binaries": ["aider"], "packages": ["pipx:aider-chat", "uv:aider-chat"],
"state_dirs": ["~/.aider"]},
"proofs": {"provenance": ["pipx:aider-chat"],
"version_probe": ["--version"], "version_shape": "^aider \\d+\\.\\d+"},
"risk_factors": ["executes_shell"]
},
{
"id": "opencode", "name": "opencode", "vendor": "opencode", "kind": "cli_agent",
"fingerprints": {"binaries": ["opencode"], "packages": ["npm:opencode-ai"],
"state_dirs": ["~/.config/opencode"]},
"proofs": {"provenance": ["npm:opencode-ai"],
"version_probe": ["--version"], "version_shape": "^\\d+\\.\\d+\\.\\d+"},
"risk_factors": ["executes_shell"]
},
{
"id": "goose", "name": "goose", "vendor": "Block", "kind": "cli_agent",
"fingerprints": {"binaries": ["goose"], "state_dirs": ["~/.config/goose"]},
"proofs": {"version_probe": ["--version"], "version_shape": "^goose \\d+\\.\\d+"},
"risk_factors": ["executes_shell"]
},
{
"id": "cursor", "name": "Cursor", "vendor": "Anysphere", "kind": "app",
"fingerprints": {"bundle_ids": ["com.todesktop.230313mzl4w4u92"],
"desktop_ids": ["cursor"], "state_dirs": ["~/.cursor"]},
"proofs": {"provenance": ["brew:cursor"]},
"risk_factors": []
},
{
"id": "claude-desktop", "name": "Claude Desktop", "vendor": "Anthropic", "kind": "app",
"fingerprints": {"bundle_ids": ["com.anthropic.claudefordesktop"],
"state_dirs": ["~/Library/Application Support/Claude"]},
"proofs": {},
"risk_factors": []
},
{
"id": "vscode", "name": "Visual Studio Code", "vendor": "Microsoft", "kind": "app",
"fingerprints": {"bundle_ids": ["com.microsoft.VSCode"], "desktop_ids": ["code"],
"state_dirs": ["~/.vscode/extensions"]},
"proofs": {},
"risk_factors": []
},
{
"id": "ollama", "name": "Ollama", "vendor": "Ollama", "kind": "model_runtime",
"fingerprints": {"binaries": ["ollama"], "ports": [11434],
"model_dirs": ["~/.ollama/models"], "state_dirs": ["~/.ollama"]},
"proofs": {"provenance": ["brew:ollama"],
"version_probe": ["--version"], "version_shape": "\\d+\\.\\d+\\.\\d+"},
"risk_factors": ["local_inference"]
},
{
"id": "lm-studio", "name": "LM Studio", "vendor": "LM Studio", "kind": "model_runtime",
"fingerprints": {"bundle_ids": ["ai.elementlabs.lmstudio"], "ports": [1234]},
"proofs": {},
"risk_factors": ["local_inference"]
},
{
"id": "continue", "name": "Continue", "vendor": "Continue", "kind": "extension",
"fingerprints": {"extension_ids": ["continue.continue"], "state_dirs": ["~/.continue"]},
"proofs": {},
"risk_factors": []
},
{
"id": "github-copilot", "name": "GitHub Copilot", "vendor": "GitHub", "kind": "extension",
"fingerprints": {"extension_ids": ["github.copilot", "github.copilot-chat"]},
"proofs": {},
"risk_factors": []
},
{
"id": "comet", "name": "Comet", "vendor": "Perplexity", "kind": "ai_browser",
"fingerprints": {"bundle_ids": ["ai.perplexity.comet"]},
"proofs": {},
"risk_factors": ["browses_autonomously"]
},
{
"id": "amp", "name": "Amp", "vendor": "Sourcegraph", "kind": "cli_agent",
"fingerprints": {"binaries": ["amp"], "packages": ["npm:@sourcegraph/amp"]}, "proofs": {}
},
{
"id": "crush", "name": "Crush", "vendor": "Charm", "kind": "cli_agent",
"fingerprints": {"binaries": ["crush"], "packages": ["brew:crush"]}, "proofs": {}
},
{
"id": "qwen-code", "name": "Qwen Code", "vendor": "Alibaba", "kind": "cli_agent",
"fingerprints": {"binaries": ["qwen"], "packages": ["npm:@qwen-code/qwen-code"]}, "proofs": {}
},
{
"id": "kilo-cli", "name": "Kilo CLI", "vendor": "Kilo Code", "kind": "cli_agent",
"fingerprints": {"binaries": ["kilo"], "packages": ["npm:@kilocode/cli"]},
"proofs": {"provenance": ["npm:@kilocode/cli"],
"version_probe": ["--version"], "version_shape": "^\\d+\\.\\d+\\.\\d+"},
"risk_factors": ["executes_shell"]
},
{
"id": "github-copilot-cli", "name": "GitHub Copilot CLI", "vendor": "GitHub", "kind": "cli_agent",
"fingerprints": {"binaries": ["github-copilot-cli"], "packages": ["npm:@github/copilot"]}, "proofs": {}
},
{
"id": "windsurf", "name": "Windsurf", "vendor": "Codeium", "kind": "app",
"fingerprints": {"bundle_ids": ["com.exafunction.windsurf"], "desktop_ids": ["windsurf"]}, "proofs": {}
},
{
"id": "zed", "name": "Zed", "vendor": "Zed Industries", "kind": "app",
"fingerprints": {"bundle_ids": ["dev.zed.Zed"], "desktop_ids": ["dev.zed.Zed", "zed"]}, "proofs": {}
},
{
"id": "trae", "name": "Trae", "vendor": "ByteDance", "kind": "app",
"fingerprints": {"bundle_ids": ["com.trae.app"], "desktop_ids": ["trae"]}, "proofs": {}
},
{
"id": "chatgpt-desktop", "name": "ChatGPT", "vendor": "OpenAI", "kind": "app",
"fingerprints": {"bundle_ids": ["com.openai.chat", "com.openai.chatgpt"]}, "proofs": {}
},
{
"id": "perplexity-desktop", "name": "Perplexity", "vendor": "Perplexity", "kind": "app",
"fingerprints": {"bundle_ids": ["ai.perplexity.mac"]}, "proofs": {}
},
{
"id": "cline", "name": "Cline", "vendor": "Cline", "kind": "extension",
"fingerprints": {"extension_ids": ["saoudrizwan.claude-dev"]}, "proofs": {}
},
{
"id": "roo-code", "name": "Roo Code", "vendor": "Roo Code", "kind": "extension",
"fingerprints": {"extension_ids": ["rooveterinaryinc.roo-cline"]}, "proofs": {}
},
{
"id": "kilo-code", "name": "Kilo Code", "vendor": "Kilo Code", "kind": "extension",
"fingerprints": {"extension_ids": ["kilocode.kilo-code"]}, "proofs": {}
},
{
"id": "jetbrains-ai", "name": "JetBrains AI Assistant", "vendor": "JetBrains", "kind": "extension",
"fingerprints": {"extension_ids": ["com.intellij.ml.llm"]}, "proofs": {}
},
{
"id": "llama-cpp", "name": "llama.cpp", "vendor": "ggml-org", "kind": "model_runtime",
"fingerprints": {"binaries": ["llama-server", "server"], "packages": ["brew:llama.cpp"]}, "proofs": {}
},
{
"id": "vllm", "name": "vLLM", "vendor": "vLLM", "kind": "model_runtime",
"fingerprints": {"binaries": ["vllm"], "packages": ["pipx:vllm", "uv:vllm"]}, "proofs": {}
},
{
"id": "gpt4all", "name": "GPT4All", "vendor": "Nomic", "kind": "model_runtime",
"fingerprints": {"bundle_ids": ["com.nomic.gpt4all"], "desktop_ids": ["gpt4all"]}, "proofs": {}
},
{
"id": "jan", "name": "Jan", "vendor": "Jan", "kind": "model_runtime",
"fingerprints": {"bundle_ids": ["com.jan.ai"], "desktop_ids": ["jan"]}, "proofs": {}
},
{
"id": "localai", "name": "LocalAI", "vendor": "LocalAI", "kind": "model_runtime",
"fingerprints": {"binaries": ["local-ai"], "packages": ["brew:localai"]}, "proofs": {}
},
{
"id": "open-webui", "name": "Open WebUI", "vendor": "Open WebUI", "kind": "app",
"fingerprints": {"packages": ["pipx:open-webui", "uv:open-webui"]}, "proofs": {}
},
{
"id": "arc", "name": "Arc", "vendor": "The Browser Company", "kind": "ai_browser",
"fingerprints": {"bundle_ids": ["company.thebrowser.Browser"]}, "proofs": {}
}
]
}
151 changes: 151 additions & 0 deletions Discovery/adr_discovery/catalog/load.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
"""C1 -- the catalog, as data.

This package imports nothing from the collector, on purpose: the landscape
changes weekly and a client release cannot, so the catalog has to ship on
its own cadence. Coupling it to the pipeline would put that churn rate on
the release train.

Every rule below is enforced *at load*. A catalog defect that first shows up
at match time has already shipped to the fleet.
"""

from __future__ import annotations

import json
import re
from dataclasses import dataclass, field


class CatalogError(ValueError):
"""Rejected at load, with the reason. Never a warning."""


@dataclass(frozen=True, slots=True)
class Entry:
id: str
name: str
vendor: str
kind: str
fingerprints: dict[str, list] = field(default_factory=dict)
proofs: dict[str, object] = field(default_factory=dict)
risk_factors: tuple[str, ...] = ()

@property
def version_probe(self) -> tuple[str, ...]:
return tuple(self.proofs.get("version_probe") or ())

@property
def version_shape(self) -> str | None:
shape = self.proofs.get("version_shape")
return str(shape) if shape else None

@property
def provenance(self) -> tuple[str, ...]:
return tuple(self.proofs.get("provenance") or ())

@property
def content_hashes(self) -> tuple[str, ...]:
return tuple(self.proofs.get("content_hashes") or ())


@dataclass(frozen=True, slots=True)
class Catalog:
version: str
entries: tuple[Entry, ...]
by_id: dict[str, Entry]
#: fingerprint kind -> value -> entry id. Built at load, which is where
#: an ambiguous value is caught.
index: dict[str, dict[str, str]]

def match(self, kind: str, value: str) -> Entry | None:
entry_id = self.index.get(kind, {}).get(str(value).lower())
return self.by_id.get(entry_id) if entry_id else None

def __len__(self) -> int:
return len(self.entries)


EMPTY = Catalog("empty", (), {}, {})

#: Fingerprint kinds that must be unique across the whole catalog.
UNIQUE_KINDS = (
"binaries", "packages", "bundle_ids", "registry_names",
"extension_ids", "desktop_ids", "ports", "model_dirs",
)

#: Proof kinds, indexed the same way and subject to the same ambiguity
#: rule. A hash claimed by two entries is the worst kind of ambiguity --
#: it is the one piece of evidence that is supposed to be conclusive.
UNIQUE_PROOFS = ("content_hashes",)

INDEXED_KINDS = UNIQUE_KINDS + UNIQUE_PROOFS


def loads(text: str) -> Catalog:
try:
document = json.loads(text)
except json.JSONDecodeError as exc:
raise CatalogError(f"catalog is not valid JSON: {exc}") from exc

version = str(document.get("version", "unknown"))
entries: list[Entry] = []
by_id: dict[str, Entry] = {}
index: dict[str, dict[str, str]] = {kind: {} for kind in INDEXED_KINDS}

for raw in document.get("entries", []):
entry = _entry(raw)
if entry.id in by_id:
raise CatalogError(f"duplicate entry id {entry.id!r}")
by_id[entry.id] = entry
entries.append(entry)

for kind in INDEXED_KINDS:
source = entry.proofs if kind in UNIQUE_PROOFS else entry.fingerprints
for value in source.get(kind, ()) or ():
key = str(value).lower()
owner = index[kind].get(key)
if owner is not None and owner != entry.id:
# Whichever loaded last would otherwise win every match,
# and the fleet is attributed to a tool nobody chose.
raise CatalogError(
f"ambiguous fingerprint {kind}:{value!r} claimed by "
f"{owner!r} and {entry.id!r}"
)
index[kind][key] = entry.id

return Catalog(version, tuple(entries), by_id, index)


def _entry(raw: object) -> Entry:
if not isinstance(raw, dict):
raise CatalogError(f"entry must be a mapping, got {type(raw).__name__}")
for required in ("id", "name", "vendor", "kind"):
if not isinstance(raw.get(required), str) or not raw[required]:
raise CatalogError(f"entry missing {required!r}: {raw.get('id', '?')}")

proofs = raw.get("proofs") or {}
if not isinstance(proofs, dict):
raise CatalogError(f"{raw['id']}: proofs must be a mapping")

probe, shape = proofs.get("version_probe"), proofs.get("version_shape")
if probe and not shape:
# An unverified probe is how a renamed `sleep` acquired version 9.4.
raise CatalogError(
f"{raw['id']}: version_probe without version_shape -- "
"a probe whose output is not checked is not a proof"
)
if shape:
try:
re.compile(str(shape))
except re.error as exc:
raise CatalogError(f"{raw['id']}: version_shape is not a valid regex: {exc}") from exc

fingerprints = raw.get("fingerprints") or {}
if not isinstance(fingerprints, dict):
raise CatalogError(f"{raw['id']}: fingerprints must be a mapping")

return Entry(
id=raw["id"], name=raw["name"], vendor=raw["vendor"], kind=raw["kind"],
fingerprints=fingerprints, proofs=proofs,
risk_factors=tuple(raw.get("risk_factors") or ()),
)
Loading