Help us shape the future of Go! Take the 2026 Go Developer Survey

skilldoc

package
v0.14.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 19, 2026 License: MIT Imports: 10 Imported by: 0

Documentation

Overview

Package skilldoc is the pure half of `hadron skill` (hadron-cli#580): the contract between a Hadron task node and the skill file exported from it. It knows how a skill's NAME is derived, how its provenance HASH is computed, what the generated file looks like, how to read one back, and which corpus rules a declaring node must satisfy. It touches neither the network nor the disk, so every rule here is table-testable; the command package wires it to GraphQL and the filesystem.

The design is in docs/plans/skill-command-group.md. The two load-bearing decisions: the node is the source and the file is a build artifact (D2), and staleness is content-addressed — the file records a hash of the exact inputs it was built from, so drift is detected by recomputation and never by clock (D5).

Index

Constants

View Source
const (
	MaxNameLen        = 64
	MaxDescriptionLen = 1024
)

Host limits for a Claude Code skill, per the skill spec (verified against the official skill-creator validator, quick_validate.py), counted in CHARACTERS — the validator is Python and counts code points, so an em dash is one, not three. The host does not REFUSE an over-long description — it truncates it in the skill listing, so the trigger phrases past the cut silently stop firing. That is why the description rule is an error rather than a warning.

View Source
const (
	SevError   = "error"
	SevWarning = "warning"
)

Finding severities — the same strings `spec lint` renders, so a caller scripting both sees one vocabulary.

View Source
const DefaultPrefix = "hadron-"

DefaultPrefix is the export namespace of a task in a USER-owned memory (Holger, 2026-09-15): a personal task has no org to name, so it takes the platform's. An org-owned task uses its org's `Organization.skillPrefix` (hadron-server#1164) and never this.

Variables

This section is empty.

Functions

func DeriveName

func DeriveName(prefix, loc string) string

DeriveName composes a skill's name from its export prefix and its node loc (D8): the leading `tasks` segment is structure rather than name and is dropped when the loc has more than one segment; the remaining segments are joined with hyphens. `tasks:create-release-tag` → `<prefix>create-release-tag`; a root-level `export-task-as-claude-skill` → `<prefix>export-task-as-claude-skill`; a nested `tasks:review:run` → `<prefix>review-run`. The result is not validated here — see ValidName — so a caller can report the derived value that failed rather than an empty string.

func Hash

func Hash(source, name, description, content string) string

Hash is the provenance fingerprint a generated file records: the first 16 hex characters of SHA-256 over the four inputs the file is made from — the source node URN, name, description and body — NUL-separated so a boundary shift cannot collide. It is deliberately not nodedoc.ContentHash (content only): the description is the trigger, so an edit to it must read as stale; and the source is included so a hand-edited provenance line that names a different node reads as a local edit rather than pairing the file with a node it was never rendered from (Codex on #589, round 12). Every input is present in the file itself, which is what lets a reader recompute the hash from the file alone and detect a hand edit without the server.

Inputs are hashed in their exported form (NormalizeDescription, NormalizeBody) — what Render writes and ParseFile reads back — so a node whose content is wrapped in blank lines and the file built from it fingerprint identically. Without that, every export would classify as stale against its own header.

func Malformed

func Malformed(props map[string]any) []string

Malformed reports the declaration keys a node carries whose value is not an object — `"skill": "yes"`, `"claudeSkill": true`. Such a node is selected by the server-side discovery predicate (the key EXISTS), so without a finding it would be silently skipped and counted in nobody's total — an all-clear wider than the read that produced it.

func NormalizeBody

func NormalizeBody(s string) string

NormalizeBody is the node content as exported: CRLF folded to LF (a body authored on Windows is the same body), then leading and trailing newlines trimmed. Render writes this, Hash fingerprints this, every body rule reads this, and ParseFile's header-stripping is its exact inverse — so a body that begins with a blank line, or carries \r\n, does not make a fresh export read as locally edited or slip past a rule written for LF.

func NormalizeDescription

func NormalizeDescription(s string) string

NormalizeDescription is the description as exported: surrounding whitespace trimmed. Lint measures this and Render writes this, so the length lint certifies is the length the host sees — a description with a trailing space is not 1025 characters on disk.

func Render

func Render(name, source, description, content string) (string, error)

Render composes the skill file (§4.3): YAML frontmatter with the two keys a host reads, the machine-parseable provenance line, the human note, and the node body NORMALIZED (NormalizeBody: CRLF folded, surrounding newlines trimmed — the same form Hash fingerprints and ParseFile reads back; inner content is untouched). The frontmatter goes through the real YAML encoder (nodedoc.MarshalYAML, the library nodedoc already depends on) rather than hand-quoting: a description ending in a colon, or in a space, is a plain scalar a hand check would pass and a real parser would reject or trim, and the host reading this file IS a real parser.

func ValidName

func ValidName(name string) bool

ValidName reports whether name satisfies the host's grammar and length.

func ValidPrefix

func ValidPrefix(p string) bool

ValidPrefix reports whether p is a legal export prefix — the server's own rule for Organization.skillPrefix, applied to a `--prefix` override so an override cannot mint a name the org field could never hold.

Types

type Declaration

type Declaration struct {
	Key         string
	Description string
	Name        string
	// NameSet records that a `name` key was present, so an explicitly EMPTY
	// stored name is judged against the derived one rather than read as
	// absent (Copilot on #589, round 2).
	NameSet bool
}

Declaration is a node's opt-in to export: `properties.skill` (D10, the provider-neutral key) or the legacy `properties.claudeSkill`, whichever is present. Key records which one, so lint can steer a legacy declaration to the new key. Name is the pre-#580 hand-set skill name; it is retired in favor of derivation (D8) and accepted during transition only when it equals the derived value.

func Declared

func Declared(props map[string]any) (*Declaration, bool)

Declared reads a node's skill declaration out of its decoded properties. A declaration is an object under one of the declaration keys; anything else — the key absent, or holding a non-object — is "not declared". The new key wins when both are present, so a migrated node whose legacy key was left behind behaves as migrated.

type File

type File struct {
	Name        string
	Description string
	Source      string // the flat v2 source node URN as written, or "" when the file is not Hadron-generated
	Hash        string // "" for a legacy (pre-#580) header
	Body        string
	// Extra holds every frontmatter key other than name/description. Render
	// writes none, so on a generated file a non-empty Extra IS a local edit
	// — one the header hash cannot see, since the hash covers the three
	// rendered inputs — and status/export must treat it as `locally-edited`
	// rather than overwrite it (Codex on #589, round 11).
	Extra map[string]any
}

File is what ParseFile reads back from a skill file on disk: the frontmatter keys a host reads, the provenance the header carries, and the body — enough to recompute the hash (Hash(Source, Name, Description, Body)) and pair the file to its source node without the server.

func ParseFile

func ParseFile(data []byte) (*File, error)

ParseFile reads a skill file with the same YAML parser a host uses, so a header this reader accepts is one the host accepts. A file with no frontmatter is an error; a file with no provenance header parses with Source == "" — it is somebody else's skill, and every `hadron skill` command leaves it alone. The legacy `Generated from` header yields a Source and no Hash.

type Finding

type Finding struct {
	URN      string
	Memory   string
	Rule     string
	Severity string
	Message  string
}

Finding is one lint result. URN names the node; for a memory-level finding (a prefix the org has not chosen) URN is the memory URN and Memory equals it. Memory is carried on every finding so a renderer never has to look it back up from the node it came from.

func Lint

func Lint(n Node, prefix Prefix) []Finding

Lint applies the per-node corpus rules to a node under the export prefix its memory resolves to. When the prefix is not Known the name rules run on the bare slug — a loc that derives to an invalid name is invalid under any prefix, and the missing prefix is LintPrefixes' finding, not this one's. A node with no declaration and no malformed key yields nothing: not declared is not a defect, it is the opt-in working.

func LintCollisions

func LintCollisions(nodes []Node, prefixes map[string]Prefix) []Finding

LintCollisions applies the corpus-level rule: two declaring nodes may not derive the same skill name, because the second export would silently win. Both members of a collision are reported, each naming the other, so whichever the reader opens explains itself.

A node whose memory has no Known prefix is skipped: its name cannot be derived, so it cannot collide — and comparing bare slugs would report two prefix-less ORGS as colliding on `create-release-tag` when the prefixes they have yet to choose are exactly what keeps them apart (measured on the first live `--all` run: marketrailz vs micromentor.org). Those nodes already carry LintPrefixes' finding.

func LintPrefixes

func LintPrefixes(nodes []Node, prefixes map[string]Prefix) []Finding

LintPrefixes applies the memory-level rule: a memory that holds at least one declaring node must have a Known prefix, or no name can be derived for its tasks. A memory with nothing to export is silent — reporting it anyway made `--all` red on 46 memories in the first live run, the report nobody reads. One finding per memory, keyed on the memory URN, in URN order.

type Node

type Node struct {
	URN        string
	Loc        string
	MemoryURN  string
	IsRunnable bool
	Content    string
	Properties map[string]any
}

Node is the slice of a Hadron node the skill contract reads. The command package fills it from a NodeBatch projection — the batched read, because a single-ref read compiles Mustache and would blank `{{name}}`-style placeholders out of a verbatim export (D6).

type Prefix

type Prefix struct {
	Value string
	Known bool
}

Prefix is a memory's resolved export prefix. Known is false when the memory is org-owned and the org has chosen no `Organization.skillPrefix`: Value is then empty, and the rules that need a prefix say so rather than deriving a name from nothing. Carrying the pair keeps "unknown" distinct from "empty" — a server that ever coerced a null prefix to "" would otherwise read as resolved (this repo's recurring nil-guard class).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL