mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-05 21:39:54 +02:00
The Go SKILL.md frontmatter parser unmarshalled into a {Name,Description}
string struct, so a non-scalar value (a list/map written where a scalar
belongs) made the whole decode fail and dropped even a valid sibling
`name`. The TS parser instead kept the name and JSON-encoded the value,
so the file-viewer (TS) and the import path (Go) could disagree about
the same SKILL.md.
Decode into a generic map and coerce per key on the Go side, mirroring
the TS coercion (scalars -> literal form, sequences/mappings -> JSON), so
both sides produce identical results and a structured value never
discards a sibling key. Rename ParseFrontmatter -> ParseSkillFrontmatter
to remove the cross-language name clash with the TS parseFrontmatter
(which returns {frontmatter, body}), and drop the unused TS
parseSkillFrontmatter export.
Add parity tests for sequence/mapping values plus name-only,
description-only, leading-blank-line and triple-dash-in-body edge cases
on both sides.
Follow-up to #3543 / MUL-2842.
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
70 lines
2.2 KiB
Go
70 lines
2.2 KiB
Go
// Package skill provides shared utilities for working with SKILL.md files.
|
|
package skill
|
|
|
|
import (
|
|
"encoding/json"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// Keeping the trailing newline inside group 1 matters: yaml.v3's `|` clip
|
|
// chomping only preserves a final newline when the input itself contains one.
|
|
var frontmatterPattern = regexp.MustCompile(`(?s)\A---\r?\n(.*?\r?\n)---`)
|
|
|
|
// ParseSkillFrontmatter extracts name and description from the YAML frontmatter
|
|
// block of a SKILL.md file. Returns empty strings when the frontmatter is
|
|
// absent or malformed so callers can keep treating missing metadata as a
|
|
// non-fatal condition, matching the behaviour of the legacy line-based parser.
|
|
//
|
|
// Values are decoded into a generic map and coerced per key (scalars via their
|
|
// literal form, sequences/mappings via JSON) rather than unmarshalled into a
|
|
// string struct. This means a structured value in one field never discards a
|
|
// valid sibling key, and the coercion mirrors the TS parseFrontmatter in
|
|
// packages/core/skills/frontmatter.ts so both sides agree on the same input.
|
|
func ParseSkillFrontmatter(content string) (name, description string) {
|
|
if !strings.HasPrefix(content, "---") {
|
|
return "", ""
|
|
}
|
|
match := frontmatterPattern.FindStringSubmatch(content)
|
|
if match == nil {
|
|
return "", ""
|
|
}
|
|
|
|
var fm map[string]any
|
|
if err := yaml.Unmarshal([]byte(match[1]), &fm); err != nil {
|
|
return "", ""
|
|
}
|
|
return coerceFrontmatterValue(fm["name"]), coerceFrontmatterValue(fm["description"])
|
|
}
|
|
|
|
// coerceFrontmatterValue renders a decoded YAML value as a string, mirroring the
|
|
// TS side: nil becomes empty, strings pass through, other scalars use their
|
|
// literal form, and structured values (sequences/mappings) are JSON-encoded.
|
|
func coerceFrontmatterValue(v any) string {
|
|
switch val := v.(type) {
|
|
case nil:
|
|
return ""
|
|
case string:
|
|
return val
|
|
case bool:
|
|
return strconv.FormatBool(val)
|
|
case int:
|
|
return strconv.Itoa(val)
|
|
case int64:
|
|
return strconv.FormatInt(val, 10)
|
|
case uint64:
|
|
return strconv.FormatUint(val, 10)
|
|
case float64:
|
|
return strconv.FormatFloat(val, 'g', -1, 64)
|
|
default:
|
|
encoded, err := json.Marshal(val)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return string(encoded)
|
|
}
|
|
}
|