Files
dingtalk-workspace-cli/internal/generator/generator.go
T

1021 lines
33 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package generator
import (
"bytes"
"encoding/json"
"fmt"
"path/filepath"
"slices"
"sort"
"strings"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/ir"
)
type Artifact struct {
Path string
Content []byte
}
type skillIndexEntry struct {
Name string
Description string
Category string
Path string
}
type helperSkillRef struct {
Name string
Path string
Tool ir.ToolDescriptor
}
type frontmatterSpec struct {
Name string
Description string
Category string
CLIHelp string
Domain string
RequiresSkills []string
}
const (
skillVersion = "1.1.0"
skillCategoryService = "service"
skillCategoryHelper = "helper"
skillCategoryPersona = "persona"
skillCategoryRecipe = "recipe"
frontmatterDescriptionLimit = 120
)
var blockedCanonicalTools = map[string]struct{}{
"drive.empty_trash": {},
"drive.delete": {},
}
var writeOperationTokens = map[string]struct{}{
"add": {},
"append": {},
"approve": {},
"batch": {},
"commit": {},
"create": {},
"delete": {},
"done": {},
"insert": {},
"issue": {},
"mkdir": {},
"modify": {},
"patch": {},
"reject": {},
"remove": {},
"replace": {},
"revoke": {},
"send": {},
"submit": {},
"sync": {},
"transfer": {},
"update": {},
"upload": {},
"write": {},
}
var legacy17CoverageTargets = []string{
"aiapp",
"aitable",
"attendance",
"calendar",
"chat",
"conference",
"contact",
"devdoc",
"ding",
"doc",
"drive",
"live",
"mail",
"minutes",
"oa",
"report",
"todo",
}
var extended22CoverageTargets = []string{
"aiapp",
"aitable",
"attendance",
"calendar",
"chat",
"conference",
"contact",
"devdoc",
"ding",
"doc",
"drive",
"live",
"mail",
"minutes",
"oa",
"report",
"todo",
"aidesign",
"finance",
"law",
"docparse",
}
func Generate(catalog ir.Catalog) ([]Artifact, error) {
artifacts := make([]Artifact, 0, len(catalog.Products)+12)
catalogJSON, err := marshalJSON(catalog)
if err != nil {
return nil, err
}
artifacts = append(artifacts, Artifact{
Path: filepath.ToSlash("docs/generated/schema/catalog.json"),
Content: catalogJSON,
})
for _, product := range catalog.Products {
for _, tool := range product.Tools {
payload := map[string]any{
"kind": "generated_schema",
"path": tool.CanonicalPath,
"product_id": product.ID,
"display": product.DisplayName,
"tool": tool,
"required": requiredFields(tool.InputSchema),
}
data, err := marshalJSON(payload)
if err != nil {
return nil, err
}
artifacts = append(artifacts, Artifact{
Path: filepath.ToSlash(filepath.Join("docs/generated/schema", tool.CanonicalPath+".json")),
Content: data,
})
artifacts = append(artifacts, Artifact{
Path: filepath.ToSlash(filepath.Join("docs/generated/schema", safeDocSegment(product.ID), safeDocSegment(tool.RPCName)+".json")),
Content: data,
})
}
}
cliDoc, err := renderCanonicalCLI(catalog)
if err != nil {
return nil, err
}
artifacts = append(artifacts, Artifact{
Path: filepath.ToSlash("docs/generated/cli/canonical-cli.md"),
Content: []byte(cliDoc),
})
for _, product := range catalog.Products {
artifacts = append(artifacts, Artifact{
Path: filepath.ToSlash(filepath.Join("docs/generated/cli", safeDocSegment(product.ID)+".md")),
Content: []byte(renderProductCLI(product)),
})
}
readme := strings.Join([]string{
"# Generated Docs",
"",
"These artifacts are generated from the shared canonical Tool IR. Do not edit them by hand.",
"",
"- `docs/generated/cli/canonical-cli.md`: canonical command surface summary",
"- `docs/generated/cli/<product>.md`: per-product canonical command summary",
"- `docs/generated/schema/catalog.json`: full catalog snapshot",
"- `docs/generated/schema/<product>.<tool>.json`: per-tool schema payloads",
"- `docs/generated/schema/<product>/<tool>.json`: per-tool schema payloads in hierarchical layout",
"- `skills/generated/apis.md`: top-level skills index for generated docs navigation",
"- `docs/generated/skills-coverage.md`: coverage report against legacy17/extended22 targets",
"",
}, "\n")
artifacts = append(artifacts, Artifact{
Path: filepath.ToSlash("docs/generated/README.md"),
Content: []byte(readme),
})
indexEntries := make([]skillIndexEntry, 0, len(catalog.Products)*4)
canonicalSkill, err := renderCanonicalSurfaceSkill(catalog)
if err != nil {
return nil, err
}
canonicalPath := filepath.ToSlash("skills/generated/canonical-surface/api.md")
artifacts = append(artifacts, Artifact{
Path: canonicalPath,
Content: []byte(canonicalSkill),
})
indexEntries = append(indexEntries, skillIndexEntry{
Name: "canonical-surface",
Description: truncateSkillDescription("DWS canonical MCP surface and schema discovery entrypoint."),
Category: skillCategoryService,
Path: canonicalPath,
})
sharedPath := filepath.ToSlash("skills/generated/dws-shared/api.md")
artifacts = append(artifacts, Artifact{
Path: sharedPath,
Content: []byte(renderSharedSkill(catalog)),
})
indexEntries = append(indexEntries, skillIndexEntry{
Name: "dws-shared",
Description: truncateSkillDescription("DWS shared reference for authentication, command patterns, and safety rules."),
Category: skillCategoryService,
Path: sharedPath,
})
availableServices := map[string]struct{}{}
helperCountsByService := map[string]int{}
for _, product := range catalog.Products {
helperRefs := make([]helperSkillRef, 0, len(product.Tools))
helperNames := map[string]int{}
for _, tool := range product.Tools {
if isBlockedTool(product, tool) {
continue
}
helperName := helperSkillName(product, tool, helperNames)
helperPath := filepath.ToSlash(filepath.Join("skills/generated", helperName, "api.md"))
helperRefs = append(helperRefs, helperSkillRef{
Name: helperName,
Path: helperPath,
Tool: tool,
})
helperMD := renderHelperSkill(product, tool, helperName)
artifacts = append(artifacts, Artifact{Path: helperPath, Content: []byte(helperMD)})
indexEntries = append(indexEntries, skillIndexEntry{
Name: helperName,
Description: truncateSkillDescription(helperDescription(product, tool)),
Category: skillCategoryHelper,
Path: helperPath,
})
}
serviceName := "dws-" + safeSkillSegment(product.ID)
servicePath := filepath.ToSlash(filepath.Join("skills/generated", serviceName, "api.md"))
serviceMD := renderServiceSkill(product, helperRefs)
artifacts = append(artifacts, Artifact{Path: servicePath, Content: []byte(serviceMD)})
indexEntries = append(indexEntries, skillIndexEntry{
Name: serviceName,
Description: truncateSkillDescription(serviceDescription(product)),
Category: skillCategoryService,
Path: servicePath,
})
serviceToken := safeSkillSegment(product.ID)
availableServices[serviceToken] = struct{}{}
helperCountsByService[serviceToken] = len(helperRefs)
}
personaRegistry, err := loadPersonaRegistry()
if err != nil {
return nil, err
}
recipeRegistry, err := loadRecipeRegistry()
if err != nil {
return nil, err
}
if err := validateRegistryReferences(personaRegistry, recipeRegistry); err != nil {
return nil, err
}
for _, persona := range personaRegistry.Personas {
safeName := "persona-" + safeSkillSegment(persona.Name)
path := filepath.ToSlash(filepath.Join("skills/generated", safeName, "api.md"))
artifacts = append(artifacts, Artifact{Path: path, Content: []byte(renderPersonaSkill(persona, safeName, availableServices))})
indexEntries = append(indexEntries, skillIndexEntry{
Name: safeName,
Description: truncateSkillDescription(persona.Description),
Category: skillCategoryPersona,
Path: path,
})
}
for _, recipe := range recipeRegistry.Recipes {
safeName := "recipe-" + safeSkillSegment(recipe.Name)
path := filepath.ToSlash(filepath.Join("skills/generated", safeName, "api.md"))
artifacts = append(artifacts, Artifact{Path: path, Content: []byte(renderRecipeSkill(recipe, safeName, availableServices))})
indexEntries = append(indexEntries, skillIndexEntry{
Name: safeName,
Description: truncateSkillDescription(recipe.Description),
Category: skillCategoryRecipe,
Path: path,
})
}
artifacts = append(artifacts, Artifact{
Path: filepath.ToSlash("skills/generated/apis.md"),
Content: []byte(renderSkillsIndex(indexEntries, "skills/generated/")),
})
artifacts = append(artifacts, Artifact{
Path: filepath.ToSlash("docs/generated/skills-coverage.md"),
Content: []byte(renderSkillsCoverageReport(catalog, availableServices, helperCountsByService)),
})
slices.SortFunc(artifacts, func(left, right Artifact) int {
return strings.Compare(left.Path, right.Path)
})
return artifacts, nil
}
func renderCanonicalCLI(catalog ir.Catalog) (string, error) {
var builder strings.Builder
builder.WriteString("# Canonical CLI Surface\n\n")
builder.WriteString("Generated from the shared Tool IR. Do not edit by hand.\n\n")
builder.WriteString("## Command Pattern\n\n")
builder.WriteString("- `dws mcp <canonical-product> <tool> --json '{...}'`\n")
builder.WriteString("- `dws schema <canonical-product>.<tool>`\n\n")
builder.WriteString("## Products\n\n")
for _, product := range catalog.Products {
builder.WriteString(fmt.Sprintf("### `%s`\n\n", product.ID))
builder.WriteString(fmt.Sprintf("- Display name: %s\n", safeValue(product.DisplayName, product.ID)))
builder.WriteString(fmt.Sprintf("- Server key: `%s`\n", product.ServerKey))
builder.WriteString(fmt.Sprintf("- Protocol: `%s`\n", safeValue(product.NegotiatedProtocolVersion, "unknown")))
builder.WriteString(fmt.Sprintf("- Degraded: `%t`\n", product.Degraded))
builder.WriteString("- Tools:\n")
for _, tool := range product.Tools {
flags := renderFlags(cli.BuildFlagSpecs(tool.InputSchema, tool.FlagHints))
builder.WriteString(fmt.Sprintf(" - `%s`: %s\n", tool.CanonicalPath, safeValue(tool.Description, tool.Title)))
builder.WriteString(fmt.Sprintf(" Flags: %s\n", flags))
builder.WriteString(fmt.Sprintf(" Schema: `%s`\n", filepath.ToSlash(filepath.Join("docs/generated/schema", safeDocSegment(product.ID), safeDocSegment(tool.RPCName)+".json"))))
}
builder.WriteString("\n")
}
return finalizeMarkdown(builder.String()), nil
}
func renderCanonicalSurfaceSkill(catalog ir.Catalog) (string, error) {
var builder strings.Builder
builder.WriteString(renderFrontmatter(frontmatterSpec{
Name: "canonical-surface",
Description: "DWS canonical MCP surface and schema discovery entrypoint.",
Category: "productivity",
CLIHelp: "dws mcp --help",
}))
builder.WriteString("# DWS Canonical Surface\n\n")
builder.WriteString("Use the canonical `dws mcp` surface generated from the shared Tool IR.\n\n")
builder.WriteString("## Command Pattern\n\n")
builder.WriteString("- `dws mcp <canonical-product> <tool> --json '{...}'`\n")
builder.WriteString("- `dws schema <canonical-product>.<tool>`\n")
builder.WriteString("- Top-level scalar and scalar-array fields may also be passed as convenience flags.\n")
builder.WriteString("- Nested objects stay in `--json` or `--params`.\n\n")
builder.WriteString("## Known Products\n\n")
for _, product := range catalog.Products {
builder.WriteString(fmt.Sprintf("- `%s`: ", product.ID))
toolNames := make([]string, 0, len(product.Tools))
for _, tool := range product.Tools {
toolNames = append(toolNames, fmt.Sprintf("`%s`", tool.RPCName))
}
builder.WriteString(strings.Join(toolNames, ", "))
builder.WriteString("\n")
}
builder.WriteString("\n## References\n\n")
builder.WriteString("- `docs/generated/cli/canonical-cli.md`\n")
builder.WriteString("- `docs/generated/schema/catalog.json`\n")
return finalizeMarkdown(builder.String()), nil
}
func renderSharedSkill(catalog ir.Catalog) string {
var builder strings.Builder
builder.WriteString(renderFrontmatter(frontmatterSpec{
Name: "dws-shared",
Description: "DWS shared reference for authentication, command patterns, and safety rules.",
Category: "productivity",
}))
builder.WriteString("# DWS Shared Reference\n\n")
builder.WriteString("## Installation\n\n")
builder.WriteString("Ensure `dws` is installed and accessible from `$PATH`.\n\n")
builder.WriteString("## Authentication\n\n")
builder.WriteString("```bash\n")
builder.WriteString("dws auth login\n")
builder.WriteString("dws auth status\n")
builder.WriteString("```\n\n")
builder.WriteString("## Global Rules\n\n")
builder.WriteString("- Output defaults to JSON. Use `--format table` for human-readable output.\n")
builder.WriteString("- Confirm with user before any write/delete/revoke action.\n")
builder.WriteString("- Never fabricate IDs; always extract from command output.\n")
builder.WriteString("- For risky operations, run a read/list check before executing write operations.\n\n")
builder.WriteString("## Command Pattern\n\n")
builder.WriteString("```bash\n")
builder.WriteString("dws mcp <product> <tool> --json '{...}'\n")
builder.WriteString("dws schema <product>.<tool> --json\n")
builder.WriteString("```\n\n")
builder.WriteString("## Services\n\n")
for _, product := range catalog.Products {
builder.WriteString(fmt.Sprintf("- `dws-%s`\n", safeSkillSegment(product.ID)))
}
builder.WriteString("\n")
return finalizeMarkdown(builder.String())
}
func renderServiceSkill(product ir.CanonicalProduct, helpers []helperSkillRef) string {
serviceName := "dws-" + safeSkillSegment(product.ID)
var builder strings.Builder
builder.WriteString(renderFrontmatter(frontmatterSpec{
Name: serviceName,
Description: serviceDescription(product),
Category: "productivity",
CLIHelp: fmt.Sprintf("dws mcp %s --help", product.ID),
}))
builder.WriteString(fmt.Sprintf("# DWS %s\n\n", product.ID))
builder.WriteString("> **PREREQUISITE:** Read `../dws-shared/api.md` for auth, command patterns, and security rules.\n\n")
builder.WriteString(fmt.Sprintf("- Display name: %s\n", safeValue(product.DisplayName, product.ID)))
builder.WriteString(fmt.Sprintf("- Endpoint: `%s`\n", safeValue(product.Endpoint, "unknown")))
builder.WriteString(fmt.Sprintf("- Protocol: `%s`\n", safeValue(product.NegotiatedProtocolVersion, "unknown")))
builder.WriteString(fmt.Sprintf("- Degraded: `%t`\n\n", product.Degraded))
builder.WriteString("```bash\n")
builder.WriteString(fmt.Sprintf("dws mcp %s <tool> --json '{...}'\n", product.ID))
builder.WriteString("```\n\n")
if len(helpers) > 0 {
builder.WriteString("## Helper Skills\n\n")
builder.WriteString("| Skill | Tool | Description |\n")
builder.WriteString("|-------|------|-------------|\n")
for _, helper := range helpers {
builder.WriteString(fmt.Sprintf("| [`%s`](../%s/api.md) | `%s` | %s |\n",
helper.Name,
filepath.Base(filepath.Dir(helper.Path)),
helper.Tool.RPCName,
safeValue(helper.Tool.Description, helper.Tool.Title),
))
}
builder.WriteString("\n")
}
builder.WriteString("## API Tools\n\n")
if len(helpers) == 0 {
builder.WriteString("No tools available.\n\n")
} else {
for _, helper := range helpers {
required := requiredFields(helper.Tool.InputSchema)
requiredText := "none"
if len(required) > 0 {
requiredText = strings.Join(wrapTicks(required), ", ")
}
builder.WriteString(fmt.Sprintf("### `%s`\n\n", helper.Tool.RPCName))
builder.WriteString(fmt.Sprintf("- Canonical path: `%s`\n", helper.Tool.CanonicalPath))
builder.WriteString(fmt.Sprintf("- Description: %s\n", safeValue(helper.Tool.Description, helper.Tool.Title)))
builder.WriteString(fmt.Sprintf("- Required fields: %s\n", requiredText))
builder.WriteString(fmt.Sprintf("- Sensitive: `%t`\n\n", helper.Tool.Sensitive))
}
}
builder.WriteString("## Discovering Commands\n\n")
builder.WriteString("```bash\n")
builder.WriteString(fmt.Sprintf("dws schema --json # list available products\ndws schema %s.<tool> --json # inspect schema\n", product.ID))
builder.WriteString("```\n")
return finalizeMarkdown(builder.String())
}
func renderHelperSkill(product ir.CanonicalProduct, tool ir.ToolDescriptor, helperName string) string {
var builder strings.Builder
builder.WriteString(renderFrontmatter(frontmatterSpec{
Name: helperName,
Description: helperDescription(product, tool),
Category: "productivity",
CLIHelp: fmt.Sprintf("dws schema %s --json", tool.CanonicalPath),
}))
builder.WriteString(fmt.Sprintf("# DWS %s %s\n\n", product.ID, tool.RPCName))
builder.WriteString("> **PREREQUISITE:** Read `../dws-shared/api.md` for auth, command patterns, and security rules.\n\n")
builder.WriteString(fmt.Sprintf("%s\n\n", safeValue(tool.Description, tool.Title)))
builder.WriteString("## Usage\n\n")
builder.WriteString("```bash\n")
builder.WriteString(fmt.Sprintf("dws mcp %s %s --json '{...}'\n", product.ID, tool.RPCName))
builder.WriteString("```\n\n")
specs := cli.BuildFlagSpecs(tool.InputSchema, tool.FlagHints)
required := requiredFields(tool.InputSchema)
if len(specs) > 0 {
builder.WriteString("## Flags\n\n")
builder.WriteString("| Flag | Required | Description |\n")
builder.WriteString("|------|----------|-------------|\n")
requiredSet := make(map[string]struct{}, len(required))
for _, field := range required {
requiredSet[field] = struct{}{}
}
for _, spec := range specs {
requiredMark := "—"
if _, ok := requiredSet[spec.PropertyName]; ok {
requiredMark = "✓"
}
builder.WriteString(fmt.Sprintf("| `--%s` | %s | %s |\n", spec.FlagName, requiredMark, safeValue(spec.Description, "-")))
}
builder.WriteString("\n")
}
if len(required) > 0 {
builder.WriteString("## Required Fields\n\n")
for _, field := range required {
builder.WriteString(fmt.Sprintf("- `%s`\n", field))
}
builder.WriteString("\n")
}
if isWriteTool(tool) {
builder.WriteString("> [!CAUTION]\n")
builder.WriteString("> This is a **write** command — confirm with the user before executing.\n\n")
}
serviceName := "dws-" + safeSkillSegment(product.ID)
builder.WriteString("## See Also\n\n")
builder.WriteString(fmt.Sprintf("- [dws-shared](../dws-shared/api.md) — Global rules\n- [%s](../%s/api.md) — Product skill\n", serviceName, serviceName))
return finalizeMarkdown(builder.String())
}
func renderPersonaSkill(persona PersonaEntry, skillName string, availableServices map[string]struct{}) string {
var builder strings.Builder
required, missing := resolveRequiredServiceSkills(persona.serviceRefs(), availableServices)
builder.WriteString(renderFrontmatter(frontmatterSpec{
Name: skillName,
Description: persona.Description,
Category: skillCategoryPersona,
RequiresSkills: required,
}))
builder.WriteString(fmt.Sprintf("# %s\n\n", persona.Title))
if len(required) > 0 {
builder.WriteString(fmt.Sprintf("> **PREREQUISITE:** Load the following skills before using this persona: %s\n\n", strings.Join(wrapTicks(required), ", ")))
}
if len(missing) > 0 {
builder.WriteString(fmt.Sprintf("> **NOTE:** Current catalog snapshot does not expose these referenced services yet: %s\n\n", strings.Join(wrapTicks(missing), ", ")))
}
builder.WriteString(fmt.Sprintf("%s\n\n", strings.TrimSpace(persona.Description)))
if len(persona.Workflows) > 0 {
builder.WriteString("## Relevant Workflows\n\n")
for _, workflow := range persona.Workflows {
workflowSkill := workflowSkillName(workflow)
if workflowSkill == "" {
continue
}
builder.WriteString(fmt.Sprintf("- [`%s`](../%s/api.md)\n", workflowSkill, workflowSkill))
}
builder.WriteString("\n")
}
builder.WriteString("## Instructions\n")
for _, instruction := range persona.Instructions {
builder.WriteString(fmt.Sprintf("- %s\n", instruction))
}
builder.WriteString("\n")
if len(persona.Tips) > 0 {
builder.WriteString("## Tips\n")
for _, tip := range persona.Tips {
builder.WriteString(fmt.Sprintf("- %s\n", tip))
}
builder.WriteString("\n")
}
return finalizeMarkdown(builder.String())
}
func renderRecipeSkill(recipe RecipeEntry, skillName string, availableServices map[string]struct{}) string {
var builder strings.Builder
required, missing := resolveRequiredServiceSkills(recipe.serviceRefs(), availableServices)
builder.WriteString(renderFrontmatter(frontmatterSpec{
Name: skillName,
Description: recipe.Description,
Category: skillCategoryRecipe,
Domain: safeSkillSegment(recipe.Category),
RequiresSkills: required,
}))
builder.WriteString(fmt.Sprintf("# %s\n\n", recipe.Title))
if len(required) > 0 {
builder.WriteString(fmt.Sprintf("> **PREREQUISITE:** Load the following skills before executing this recipe: %s\n\n", strings.Join(wrapTicks(required), ", ")))
}
if len(missing) > 0 {
builder.WriteString(fmt.Sprintf("> **NOTE:** Current catalog snapshot does not expose these referenced services yet: %s\n\n", strings.Join(wrapTicks(missing), ", ")))
}
builder.WriteString(fmt.Sprintf("%s\n\n", strings.TrimSpace(recipe.Description)))
if strings.TrimSpace(recipe.Caution) != "" {
builder.WriteString(fmt.Sprintf("> [!CAUTION]\n> %s\n\n", strings.TrimSpace(recipe.Caution)))
}
builder.WriteString("## Steps\n\n")
for i, step := range recipe.Steps {
builder.WriteString(fmt.Sprintf("%d. %s\n", i+1, strings.TrimSpace(step)))
}
builder.WriteString("\n")
return finalizeMarkdown(builder.String())
}
func renderSkillsIndex(entries []skillIndexEntry, linkBase string) string {
slices.SortFunc(entries, func(left, right skillIndexEntry) int {
if left.Category != right.Category {
return strings.Compare(left.Category, right.Category)
}
return strings.Compare(left.Name, right.Name)
})
var builder strings.Builder
builder.WriteString("# Skills Index\n\n")
builder.WriteString("> Auto-generated by `dws generate-skills`. Do not edit manually.\n\n")
sections := []struct {
Category string
Heading string
SubTitle string
}{
{Category: skillCategoryService, Heading: "## Services", SubTitle: "Core DWS product and shared skills."},
{Category: skillCategoryHelper, Heading: "## Helpers", SubTitle: "Tool-level execution skills."},
{Category: skillCategoryPersona, Heading: "## Personas", SubTitle: "Role-based skill bundles."},
{Category: skillCategoryRecipe, Heading: "## Recipes", SubTitle: "Multi-step task sequences with concrete commands."},
}
for _, section := range sections {
items := make([]skillIndexEntry, 0)
for _, entry := range entries {
if entry.Category == section.Category {
items = append(items, entry)
}
}
if len(items) == 0 {
continue
}
builder.WriteString(section.Heading)
builder.WriteString("\n\n")
builder.WriteString(section.SubTitle)
builder.WriteString("\n\n")
builder.WriteString("| Skill | Description |\n")
builder.WriteString("|-------|-------------|\n")
for _, entry := range items {
builder.WriteString(fmt.Sprintf("| [%s](%s) | %s |\n", entry.Name, indexRelativeLink(entry.Path, linkBase), entry.Description))
}
builder.WriteString("\n")
}
return finalizeMarkdown(builder.String())
}
func renderSkillsCoverageReport(catalog ir.Catalog, availableServices map[string]struct{}, helperCounts map[string]int) string {
var builder strings.Builder
builder.WriteString("# Skills Coverage Report\n\n")
builder.WriteString("> Auto-generated by `dws generate-skills`. Do not edit manually.\n\n")
builder.WriteString(fmt.Sprintf("- Catalog products: `%d`\n", len(catalog.Products)))
builder.WriteString(fmt.Sprintf("- Generated services: `%d`\n", len(availableServices)))
totalHelpers := 0
for _, count := range helperCounts {
totalHelpers += count
}
builder.WriteString(fmt.Sprintf("- Generated helpers: `%d`\n\n", totalHelpers))
renderCoverageGroup(&builder, "Legacy17", legacy17CoverageTargets, availableServices, helperCounts)
renderCoverageGroup(&builder, "Extended22", extended22CoverageTargets, availableServices, helperCounts)
return finalizeMarkdown(builder.String())
}
func renderCoverageGroup(
builder *strings.Builder,
name string,
targets []string,
availableServices map[string]struct{},
helperCounts map[string]int,
) {
covered := make([]string, 0, len(targets))
missing := make([]string, 0)
zeroHelper := make([]string, 0)
for _, target := range uniqueSkillProducts(targets) {
if _, ok := availableServices[target]; !ok {
missing = append(missing, target)
continue
}
covered = append(covered, target)
if helperCounts[target] == 0 {
zeroHelper = append(zeroHelper, target)
}
}
sort.Strings(covered)
sort.Strings(missing)
sort.Strings(zeroHelper)
total := len(uniqueSkillProducts(targets))
builder.WriteString(fmt.Sprintf("## %s\n\n", name))
builder.WriteString(fmt.Sprintf("- Coverage: `%d/%d`\n", len(covered), total))
builder.WriteString(fmt.Sprintf("- Coverage rate: `%.1f%%`\n", coverageRate(len(covered), total)))
if len(missing) == 0 {
builder.WriteString("- Missing: `none`\n")
} else {
builder.WriteString(fmt.Sprintf("- Missing: %s\n", strings.Join(wrapTicks(missing), ", ")))
}
if len(zeroHelper) == 0 {
builder.WriteString("- Services with 0 helpers: `none`\n\n")
} else {
builder.WriteString(fmt.Sprintf("- Services with 0 helpers: %s\n\n", strings.Join(wrapTicks(zeroHelper), ", ")))
}
}
func coverageRate(covered, total int) float64 {
if total == 0 {
return 100
}
return (float64(covered) / float64(total)) * 100
}
func renderProductCLI(product ir.CanonicalProduct) string {
lines := []string{
fmt.Sprintf("# Canonical Product: %s", product.ID),
"",
"Generated from shared Tool IR. Do not edit by hand.",
"",
fmt.Sprintf("- Display name: %s", safeValue(product.DisplayName, product.ID)),
fmt.Sprintf("- Server key: `%s`", product.ServerKey),
fmt.Sprintf("- Endpoint: `%s`", product.Endpoint),
fmt.Sprintf("- Protocol: `%s`", safeValue(product.NegotiatedProtocolVersion, "unknown")),
fmt.Sprintf("- Degraded: `%t`", product.Degraded),
"",
"## Tools",
"",
}
for _, tool := range product.Tools {
lines = append(lines, fmt.Sprintf("- `%s`", tool.RPCName))
lines = append(lines, fmt.Sprintf(" - Path: `%s`", tool.CanonicalPath))
lines = append(lines, fmt.Sprintf(" - Description: %s", safeValue(tool.Description, tool.Title)))
lines = append(lines, fmt.Sprintf(" - Flags: %s", renderFlags(cli.BuildFlagSpecs(tool.InputSchema, tool.FlagHints))))
lines = append(lines, fmt.Sprintf(" - Schema: `%s`", filepath.ToSlash(filepath.Join("docs/generated/schema", safeDocSegment(product.ID), safeDocSegment(tool.RPCName)+".json"))))
}
lines = append(lines, "")
return finalizeMarkdown(strings.Join(lines, "\n"))
}
func renderFlags(specs []cli.FlagSpec) string {
if len(specs) == 0 {
return "none"
}
flags := make([]string, 0, len(specs))
for _, spec := range specs {
label := fmt.Sprintf("`--%s`", spec.FlagName)
if spec.Shorthand != "" {
label = label + fmt.Sprintf(" (`-%s`)", spec.Shorthand)
}
if spec.Alias != "" && spec.Alias != spec.FlagName {
label = label + fmt.Sprintf(" alias `--%s`", spec.Alias)
}
flags = append(flags, label)
}
return strings.Join(flags, ", ")
}
func safeSkillSegment(value string) string {
value = strings.TrimSpace(strings.ToLower(value))
if value == "" {
return "unknown"
}
var b strings.Builder
for _, r := range value {
switch {
case r >= 'a' && r <= 'z':
b.WriteRune(r)
case r >= '0' && r <= '9':
b.WriteRune(r)
default:
b.WriteByte('-')
}
}
out := strings.Trim(b.String(), "-")
if out == "" {
return "unknown"
}
return out
}
func safeDocSegment(value string) string {
value = strings.TrimSpace(value)
if value == "" {
return "unknown"
}
var b strings.Builder
for _, r := range value {
switch r {
case '/', '\\', ':':
b.WriteByte('-')
default:
b.WriteRune(r)
}
}
out := strings.Trim(b.String(), "-")
if out == "" {
return "unknown"
}
return out
}
func requiredFields(schema map[string]any) []string {
raw, ok := schema["required"].([]any)
if !ok {
return nil
}
out := make([]string, 0, len(raw))
for _, entry := range raw {
value, ok := entry.(string)
if ok && value != "" {
out = append(out, value)
}
}
sort.Strings(out)
return out
}
func marshalJSON(value any) ([]byte, error) {
data, err := json.MarshalIndent(value, "", " ")
if err != nil {
return nil, err
}
return append(data, '\n'), nil
}
func safeValue(value, fallback string) string {
value = strings.TrimSpace(value)
if value == "" {
return fallback
}
return value
}
func serviceDescription(product ir.CanonicalProduct) string {
base := safeValue(product.DisplayName, product.ID)
desc := strings.TrimSpace(product.Description)
if desc == "" {
desc = fmt.Sprintf("DWS %s service.", product.ID)
}
if strings.Contains(strings.ToLower(desc), strings.ToLower(base)) {
return truncateSkillDescription(desc)
}
return truncateSkillDescription(fmt.Sprintf("%s: %s", base, desc))
}
func helperDescription(product ir.CanonicalProduct, tool ir.ToolDescriptor) string {
base := safeValue(product.DisplayName, product.ID)
desc := safeValue(tool.Description, tool.Title)
if desc == "" {
desc = tool.RPCName
}
return truncateSkillDescription(fmt.Sprintf("%s: %s", base, desc))
}
func helperSkillName(product ir.CanonicalProduct, tool ir.ToolDescriptor, seen map[string]int) string {
token := safeSkillSegment(tool.CLIName)
if token == "unknown" {
token = safeSkillSegment(tool.RPCName)
}
name := fmt.Sprintf("dws-%s-%s", safeSkillSegment(product.ID), token)
if _, ok := seen[name]; !ok {
seen[name] = 1
return name
}
seen[name]++
return fmt.Sprintf("%s-%d", name, seen[name])
}
func isBlockedTool(product ir.CanonicalProduct, tool ir.ToolDescriptor) bool {
canonical := strings.ToLower(strings.TrimSpace(tool.CanonicalPath))
if canonical == "" {
canonical = strings.ToLower(strings.TrimSpace(fmt.Sprintf("%s.%s", product.ID, tool.RPCName)))
}
_, blocked := blockedCanonicalTools[canonical]
return blocked
}
func isWriteTool(tool ir.ToolDescriptor) bool {
if tool.Sensitive {
return true
}
parts := strings.FieldsFunc(strings.ToLower(tool.RPCName), func(r rune) bool {
return r == '_' || r == '-' || r == '.'
})
for _, part := range parts {
if _, ok := writeOperationTokens[part]; ok {
return true
}
}
return false
}
func renderFrontmatter(spec frontmatterSpec) string {
var builder strings.Builder
builder.WriteString("---\n")
builder.WriteString(fmt.Sprintf("description: \"%s\"\n", sanitizeSkillDescription(spec.Description)))
if strings.TrimSpace(spec.CLIHelp) != "" {
builder.WriteString(fmt.Sprintf("cliHelp: \"%s\"\n", strings.TrimSpace(spec.CLIHelp)))
}
builder.WriteString("---\n\n")
return builder.String()
}
func finalizeMarkdown(markdown string) string {
markdown = strings.ReplaceAll(markdown, "\r\n", "\n")
markdown = strings.ReplaceAll(markdown, "\r", "\n")
lines := strings.Split(markdown, "\n")
for idx, line := range lines {
lines[idx] = strings.TrimRight(line, " \t")
}
markdown = strings.Join(lines, "\n")
return strings.TrimRight(markdown, "\n") + "\n"
}
func sanitizeSkillDescription(description string) string {
description = strings.ReplaceAll(description, "\"", "'")
description = strings.TrimSpace(description)
if description == "" {
description = "DWS generated skill."
}
description = truncateSkillDescription(description)
if !hasTerminalPunctuation(description) {
description += "."
}
return description
}
func truncateSkillDescription(description string) string {
description = strings.TrimSpace(description)
if description == "" {
return ""
}
runes := []rune(description)
if len(runes) <= frontmatterDescriptionLimit {
return description
}
return strings.TrimSpace(string(runes[:frontmatterDescriptionLimit-1])) + "…"
}
func hasTerminalPunctuation(value string) bool {
for _, suffix := range []string{".", "…", "!", "?", "。", "!", "?"} {
if strings.HasSuffix(value, suffix) {
return true
}
}
return false
}
func workflowSkillName(value string) string {
token := normalizeRegistryToken(value)
if token == "" {
return ""
}
token = strings.TrimPrefix(token, "recipe-")
token = safeSkillSegment(token)
if token == "unknown" {
return ""
}
return "recipe-" + token
}
func indexRelativeLink(entryPath, linkBase string) string {
entryPath = filepath.ToSlash(strings.TrimSpace(entryPath))
if entryPath == "" {
return ""
}
linkBase = filepath.ToSlash(strings.TrimSpace(linkBase))
return strings.TrimPrefix(entryPath, linkBase)
}
func resolveRequiredServiceSkills(services []string, available map[string]struct{}) ([]string, []string) {
required := make([]string, 0, len(services))
missing := make([]string, 0)
for _, service := range uniqueSkillProducts(services) {
token := safeSkillSegment(service)
if token == "unknown" {
continue
}
skill := "dws-" + token
if len(available) == 0 {
required = append(required, skill)
continue
}
if _, ok := available[token]; ok {
required = append(required, skill)
} else {
missing = append(missing, skill)
}
}
return required, missing
}
func wrapTicks(values []string) []string {
out := make([]string, 0, len(values))
for _, value := range values {
value = strings.TrimSpace(value)
if value == "" {
continue
}
out = append(out, fmt.Sprintf("`%s`", value))
}
return out
}
func WriteArtifacts(root string, artifacts []Artifact) error {
for _, artifact := range artifacts {
target := filepath.Join(root, artifact.Path)
if err := writeFile(target, artifact.Content); err != nil {
return err
}
}
return nil
}
func writeFile(path string, content []byte) error {
var buffer bytes.Buffer
buffer.Write(content)
return writeFileBytes(path, buffer.Bytes())
}