docs(flows): DoD obligatorio con user-facing surface + abrir issues 0100-0103 (taxonomia, frontmatter migration, dev_console, work dashboard)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-17 00:07:04 +02:00
parent 2740f4a41a
commit 46577a6e3e
3 changed files with 121 additions and 21 deletions
+35
View File
@@ -110,6 +110,41 @@ func renderTypeMarkdown(t *registry.Type) string {
return b.String()
}
// renderModuleMarkdown returns a markdown card for a Module.
func renderModuleMarkdown(m *registry.Module) string {
var b strings.Builder
fmt.Fprintf(&b, "# %s (module)\n\n", m.ID)
fmt.Fprintf(&b, "- name: %s\n", m.Name)
fmt.Fprintf(&b, "- version: %s\n", m.Version)
fmt.Fprintf(&b, "- lang: %s\n", m.Lang)
if len(m.Members) > 0 {
fmt.Fprintf(&b, "- members: %s\n", strings.Join(m.Members, ", "))
}
if len(m.Tags) > 0 {
fmt.Fprintf(&b, "- tags: %s\n", strings.Join(m.Tags, ", "))
}
if m.DirPath != "" {
fmt.Fprintf(&b, "- dir_path: %s\n", m.DirPath)
}
if m.RepoURL != "" {
fmt.Fprintf(&b, "- repo_url: %s\n", m.RepoURL)
}
b.WriteString("\n")
if m.Description != "" {
b.WriteString(m.Description)
b.WriteString("\n\n")
}
if m.Documentation != "" {
b.WriteString(m.Documentation)
b.WriteString("\n\n")
}
if m.Notes != "" {
fmt.Fprintf(&b, "\n## notes\n\n%s\n", m.Notes)
}
return b.String()
}
// langFence maps registry lang codes to markdown fence labels.
func langFence(lang string) string {
switch lang {
+64 -5
View File
@@ -18,6 +18,7 @@ type searchArgs struct {
Purity string `json:"purity,omitempty"`
Tag string `json:"tag,omitempty"`
Tags string `json:"tags,omitempty"`
Entity string `json:"entity,omitempty"`
Limit int `json:"limit,omitempty"`
}
@@ -28,10 +29,11 @@ type searchHit struct {
Lang string `json:"lang"`
Domain string `json:"domain"`
Purity string `json:"purity"`
Version string `json:"version,omitempty"`
Signature string `json:"signature,omitempty"`
Description string `json:"description"`
Algebraic string `json:"algebraic,omitempty"`
Entity string `json:"entity"` // "function" | "type"
Entity string `json:"entity"` // "function" | "type" | "module"
}
func searchTool() mcp.Tool {
@@ -61,6 +63,10 @@ func searchTool() mcp.Tool {
mcp.WithString("tags",
mcp.Description("Filter by multiple tags (CSV). AND across tags: all must be present. Ej: 'metabase,client'."),
),
mcp.WithString("entity",
mcp.Description("Restrict search to a single entity type: functions | types | modules. Default: functions + types."),
mcp.Enum("functions", "types", "modules"),
),
mcp.WithNumber("limit",
mcp.Description("Max hits returned (default 50)."),
mcp.Min(1),
@@ -79,12 +85,46 @@ func (d *deps) handleSearch(ctx context.Context, _ mcp.CallToolRequest, args sea
tagFilters := parseTagFilters(args.Tag, args.Tags)
var hits []searchHit
// Modules-only path.
if args.Entity == "modules" {
mods, err := d.db.SearchModules(q, args.Lang)
if err != nil {
return mcp.NewToolResultError("search modules: " + err.Error()), nil
}
for _, m := range mods {
if !matchAllTags(m.Tags, tagFilters) {
continue
}
hits = append(hits, searchHit{
ID: m.ID,
Name: m.Name,
Lang: m.Lang,
Version: m.Version,
Description: m.Description,
Entity: "module",
})
if len(hits) >= limit {
break
}
}
out := map[string]any{
"query": args.Query,
"count": len(hits),
"limit": limit,
"results": hits,
}
b, _ := json.MarshalIndent(out, "", " ")
return mcp.NewToolResultText(string(b)), nil
}
// Default path: functions + types (optionally filtered to one of them).
if args.Entity == "" || args.Entity == "functions" {
fns, err := d.db.SearchFunctions(q, registry.Kind(args.Kind), registry.Purity(args.Purity), args.Lang, args.Domain, tagFilters...)
if err != nil {
return mcp.NewToolResultError("search functions: " + err.Error()), nil
}
var hits []searchHit
for _, f := range fns {
hits = append(hits, searchHit{
ID: f.ID,
@@ -101,9 +141,10 @@ func (d *deps) handleSearch(ctx context.Context, _ mcp.CallToolRequest, args sea
break
}
}
}
// Types: only when no kind filter (kind applies only to functions).
if args.Kind == "" && len(hits) < limit {
// Types: when no kind filter (kind applies only to functions) and entity allows.
if args.Kind == "" && len(hits) < limit && (args.Entity == "" || args.Entity == "types") {
ts, err := d.db.SearchTypes(q, args.Lang, args.Domain, tagFilters...)
if err != nil {
return mcp.NewToolResultError("search types: " + err.Error()), nil
@@ -134,6 +175,24 @@ func (d *deps) handleSearch(ctx context.Context, _ mcp.CallToolRequest, args sea
return mcp.NewToolResultText(string(b)), nil
}
// matchAllTags returns true if every tag in want is present in have.
// Empty want matches everything.
func matchAllTags(have, want []string) bool {
if len(want) == 0 {
return true
}
set := make(map[string]bool, len(have))
for _, t := range have {
set[t] = true
}
for _, w := range want {
if !set[w] {
return false
}
}
return true
}
// parseTagFilters merges single `tag` and CSV `tags` into a deduplicated slice.
func parseTagFilters(tag, csv string) []string {
seen := map[string]bool{}
+6
View File
@@ -37,5 +37,11 @@ func (d *deps) handleShow(ctx context.Context, _ mcp.CallToolRequest, args showA
b, _ := json.MarshalIndent(out, "", " ")
return mcp.NewToolResultText(string(b)), nil
}
if m, err := d.db.GetModule(args.ID); err == nil {
md := truncate(renderModuleMarkdown(m), 50_000)
out := map[string]any{"id": m.ID, "entity": "module", "markdown": md}
b, _ := json.MarshalIndent(out, "", " ")
return mcp.NewToolResultText(string(b)), nil
}
return mcp.NewToolResultError("id not found: " + args.ID), nil
}