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:
@@ -110,6 +110,41 @@ func renderTypeMarkdown(t *registry.Type) string {
|
|||||||
return b.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.
|
// langFence maps registry lang codes to markdown fence labels.
|
||||||
func langFence(lang string) string {
|
func langFence(lang string) string {
|
||||||
switch lang {
|
switch lang {
|
||||||
|
|||||||
+80
-21
@@ -18,6 +18,7 @@ type searchArgs struct {
|
|||||||
Purity string `json:"purity,omitempty"`
|
Purity string `json:"purity,omitempty"`
|
||||||
Tag string `json:"tag,omitempty"`
|
Tag string `json:"tag,omitempty"`
|
||||||
Tags string `json:"tags,omitempty"`
|
Tags string `json:"tags,omitempty"`
|
||||||
|
Entity string `json:"entity,omitempty"`
|
||||||
Limit int `json:"limit,omitempty"`
|
Limit int `json:"limit,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,10 +29,11 @@ type searchHit struct {
|
|||||||
Lang string `json:"lang"`
|
Lang string `json:"lang"`
|
||||||
Domain string `json:"domain"`
|
Domain string `json:"domain"`
|
||||||
Purity string `json:"purity"`
|
Purity string `json:"purity"`
|
||||||
|
Version string `json:"version,omitempty"`
|
||||||
Signature string `json:"signature,omitempty"`
|
Signature string `json:"signature,omitempty"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Algebraic string `json:"algebraic,omitempty"`
|
Algebraic string `json:"algebraic,omitempty"`
|
||||||
Entity string `json:"entity"` // "function" | "type"
|
Entity string `json:"entity"` // "function" | "type" | "module"
|
||||||
}
|
}
|
||||||
|
|
||||||
func searchTool() mcp.Tool {
|
func searchTool() mcp.Tool {
|
||||||
@@ -61,6 +63,10 @@ func searchTool() mcp.Tool {
|
|||||||
mcp.WithString("tags",
|
mcp.WithString("tags",
|
||||||
mcp.Description("Filter by multiple tags (CSV). AND across tags: all must be present. Ej: 'metabase,client'."),
|
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.WithNumber("limit",
|
||||||
mcp.Description("Max hits returned (default 50)."),
|
mcp.Description("Max hits returned (default 50)."),
|
||||||
mcp.Min(1),
|
mcp.Min(1),
|
||||||
@@ -79,31 +85,66 @@ func (d *deps) handleSearch(ctx context.Context, _ mcp.CallToolRequest, args sea
|
|||||||
|
|
||||||
tagFilters := parseTagFilters(args.Tag, args.Tags)
|
tagFilters := parseTagFilters(args.Tag, args.Tags)
|
||||||
|
|
||||||
fns, err := d.db.SearchFunctions(q, registry.Kind(args.Kind), registry.Purity(args.Purity), args.Lang, args.Domain, tagFilters...)
|
var hits []searchHit
|
||||||
if err != nil {
|
|
||||||
return mcp.NewToolResultError("search functions: " + err.Error()), nil
|
// 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
|
||||||
}
|
}
|
||||||
|
|
||||||
var hits []searchHit
|
// Default path: functions + types (optionally filtered to one of them).
|
||||||
for _, f := range fns {
|
if args.Entity == "" || args.Entity == "functions" {
|
||||||
hits = append(hits, searchHit{
|
fns, err := d.db.SearchFunctions(q, registry.Kind(args.Kind), registry.Purity(args.Purity), args.Lang, args.Domain, tagFilters...)
|
||||||
ID: f.ID,
|
if err != nil {
|
||||||
Name: f.Name,
|
return mcp.NewToolResultError("search functions: " + err.Error()), nil
|
||||||
Kind: string(f.Kind),
|
}
|
||||||
Lang: f.Lang,
|
for _, f := range fns {
|
||||||
Domain: f.Domain,
|
hits = append(hits, searchHit{
|
||||||
Purity: string(f.Purity),
|
ID: f.ID,
|
||||||
Signature: f.Signature,
|
Name: f.Name,
|
||||||
Description: f.Description,
|
Kind: string(f.Kind),
|
||||||
Entity: "function",
|
Lang: f.Lang,
|
||||||
})
|
Domain: f.Domain,
|
||||||
if len(hits) >= limit {
|
Purity: string(f.Purity),
|
||||||
break
|
Signature: f.Signature,
|
||||||
|
Description: f.Description,
|
||||||
|
Entity: "function",
|
||||||
|
})
|
||||||
|
if len(hits) >= limit {
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Types: only when no kind filter (kind applies only to functions).
|
// Types: when no kind filter (kind applies only to functions) and entity allows.
|
||||||
if args.Kind == "" && len(hits) < limit {
|
if args.Kind == "" && len(hits) < limit && (args.Entity == "" || args.Entity == "types") {
|
||||||
ts, err := d.db.SearchTypes(q, args.Lang, args.Domain, tagFilters...)
|
ts, err := d.db.SearchTypes(q, args.Lang, args.Domain, tagFilters...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return mcp.NewToolResultError("search types: " + err.Error()), 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
|
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.
|
// parseTagFilters merges single `tag` and CSV `tags` into a deduplicated slice.
|
||||||
func parseTagFilters(tag, csv string) []string {
|
func parseTagFilters(tag, csv string) []string {
|
||||||
seen := map[string]bool{}
|
seen := map[string]bool{}
|
||||||
|
|||||||
@@ -37,5 +37,11 @@ func (d *deps) handleShow(ctx context.Context, _ mcp.CallToolRequest, args showA
|
|||||||
b, _ := json.MarshalIndent(out, "", " ")
|
b, _ := json.MarshalIndent(out, "", " ")
|
||||||
return mcp.NewToolResultText(string(b)), nil
|
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
|
return mcp.NewToolResultError("id not found: " + args.ID), nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user