feat: replace category with a multi-facet tag vocabulary

A single category could not describe tools that ship as a CLI, an editor
plugin and a desktop app at once, and 7 of the 40 entries were filed
under a surface they only partly match — cline is "an SDK, IDE extension
or CLI assistant" in one `extension` slot, Reasonix ships CLI, desktop
and VS Code under `cli`, warp is a terminal filed as `ide`.

Tags cover five facets: surface (at least one), model access, workflow,
integration, and origin (at most one). tagVocabulary in validate.go is
the single source of truth — the validator, its error messages, and the
dashboard's filter chips all derive from it, the last via a new facets
field in site/data.json.

The dashboard now filters on tags with multi-select chips grouped by
facet: OR within a facet, AND across facets, plus a clear-filters
control, and search matches tags as well as name and description. Only
tags some row actually carries get a chip, so chips and rows cannot
disagree. Tag pills are tinted per facet, replacing the category badge.

All 40 entries are tagged from each repo's own README, topics and docs;
CONTRIBUTING documents every tag and the evidence rule for applying one.
A leftover `category:` key now fails validation with a message naming
its replacement, rather than being silently ignored.

README.md and data/history.jsonl are the regenerated updater output.
This commit is contained in:
tiennm99 committed 2026-09-11 15:40:18 +07:00
1 parent e49c487700
commit eb12bcc2cb
16 files changed
+462 -205

No files matched your search

+12 -12
View File
@@ -49,7 +49,7 @@ func TestFetchStats_ChunkingHappyPath(t *testing.T) {
const n = 55
agents := make([]Agent, n)
for i := range agents {
agents[i] = Agent{Owner: "org", Repo: fmt.Sprintf("repo%02d", i), Category: "cli"}
agents[i] = Agent{Owner: "org", Repo: fmt.Sprintf("repo%02d", i), Tags: []string{"terminal", "community"}}
}
var requestCount int
@@ -118,9 +118,9 @@ func TestFetchStats_ChunkingHappyPath(t *testing.T) {
func TestFetchStats_MissingNodeNamesTheRepo(t *testing.T) {
agents := []Agent{
{Owner: "foo", Repo: "bar0", Category: "cli"},
{Owner: "foo", Repo: "bar1", Category: "cli"},
{Owner: "foo", Repo: "bar2", Category: "cli"},
{Owner: "foo", Repo: "bar0", Tags: []string{"terminal", "community"}},
{Owner: "foo", Repo: "bar1", Tags: []string{"terminal", "community"}},
{Owner: "foo", Repo: "bar2", Tags: []string{"terminal", "community"}},
}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -155,9 +155,9 @@ func TestFetchStats_MissingNodeNamesTheRepo(t *testing.T) {
func TestFetchStats_GraphQLErrorNamesTheRepo(t *testing.T) {
agents := []Agent{
{Owner: "foo", Repo: "bar0", Category: "cli"},
{Owner: "foo", Repo: "bar1", Category: "cli"},
{Owner: "foo", Repo: "bar2", Category: "cli"},
{Owner: "foo", Repo: "bar0", Tags: []string{"terminal", "community"}},
{Owner: "foo", Repo: "bar1", Tags: []string{"terminal", "community"}},
{Owner: "foo", Repo: "bar2", Tags: []string{"terminal", "community"}},
}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -193,8 +193,8 @@ func TestFetchStats_DriftWarnings(t *testing.T) {
// still succeeds and reports the drift as a warning (checked via
// captured stdout) rather than failing the whole daily update.
agents := []Agent{
{Owner: "old-owner", Repo: "renamed-repo", Category: "cli"},
{Owner: "org", Repo: "archived-repo", Category: "cli"},
{Owner: "old-owner", Repo: "renamed-repo", Tags: []string{"terminal", "community"}},
{Owner: "org", Repo: "archived-repo", Tags: []string{"terminal", "community"}},
}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -261,9 +261,9 @@ func TestFetchStats_StaleWarning(t *testing.T) {
defer func() { timeNow = origNow }()
agents := []Agent{
{Owner: "org", Repo: "fresh", Category: "cli"},
{Owner: "org", Repo: "stale", Category: "cli"},
{Owner: "org", Repo: "stale-and-archived", Category: "cli"},
{Owner: "org", Repo: "fresh", Tags: []string{"terminal", "community"}},
{Owner: "org", Repo: "stale", Tags: []string{"terminal", "community"}},
{Owner: "org", Repo: "stale-and-archived", Tags: []string{"terminal", "community"}},
}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {