urn

package module
v0.0.17 Latest Latest
Warning

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

Go to latest
Published: Oct 12, 2026 License: MIT Imports: 4 Imported by: 0

README

urn-lib-go

Hadron URN compose / parse / normalize for Go.

Paired with urn-lib-js. Both implementations run the same conformance corpus (fixtures/corpus.json) — the corpus is the contract, so the two languages cannot drift. This exists to replace the copy-pasted URN parsers that had already drifted across hadron-server / portal / docs / cli (hadron-server#239, #693).

Status

Increment 1 — v1-parity core. Ports the pure, self-contained slice of hadron-server/src/lib/urn.ts verbatim, behind the shared corpus:

  • scheme — HasSchemePrefix, NormalizeScheme, CanonicalScheme, LegacyScheme
  • registry — the locked type registry (CoreTypes, RoleMarkers, reserved slugs, …)
  • normalize — NormalizeUrnForLookup, LegacyMemoryUrnToCanonical, AgentSlugFromUrn
  • slug — ValidateAtomShape, ValidateUserSlug, ValidateOrgSlug, DeriveSlugFromName
  • legacy — ParseUrnInput, FormatUrn, ValidateUrnType (the pre-021 surface)
  • parser — ParseUrn (the canonical-form parser, D11 cats 1 + 4), IsParserCanonical, ToParserCanonical
  • compose — FormatCanonicalUrn, ComposeNodeUrn, ComposeEdgeUrn
  • errors — ParseError + the machine-stable Reason codes

Not yet ported (later increments, gated by the same corpus): the qualification family (AssertFullyQualifiedUrn, SplitNodeUrn) and the install/migration helpers, then the grammar-v2 flat forms (hadron-server#694).

Usage

import urn "github.com/hadron-memory/urn-lib-go"

urn.NormalizeUrnForLookup("acme.com::specs::cor:urn") // "acme.com:specs:cor:urn"

if err := urn.ValidateOrgSlug("Acme.com"); err != nil {
    var pe *urn.ParseError
    if errors.As(err, &pe) {
        fmt.Println(pe.Reason) // "slug-not-lowercase"
    }
}

The conformance corpus

fixtures/corpus.json is a synced copy of the canonical corpus in urn-lib-js. Each case is { fn, in, out?, throws? }; corpus_test.go runs every case. Add behavior by adding a corpus case, not by editing a test in one language.

go test ./...

Tickers and agent tasks (grammar v2)

hrn:ticker:<root>:<app-slug>:<ticker-slug> and hrn:agenttask:<root>:<app-slug>:<task-id> each have exactly two segments after the root. Use ComposeTickerUrnV2 with the ticker's permanent derived slug and ComposeAgentTaskUrnV2 with the task's id. Both parse through ParseUrnV2, render their own display badge, and normalize a legacy urn: prefix to hrn:. They have no v1 equivalent; ParseUrn rejects them with unknown-type.

Task ids follow the shared URN atom rule (letters/digits, dots, underscores and hyphens, at most 64 characters). A 32-hex or cuid id is valid in the task-id leaf; root-name registration restrictions do not apply to that leaf.

Root registration policy

ValidateOrgSlug admits dotted and undotted organization names. Both it and ValidateUserHandle reject 32-hex/cuid-shaped roots (root-id-shaped) and the reserved root words hrn/urn (reserved-word-slug). New handles remain dot-free. Bare-name, lowercase, charset and length checks remain in place.

This is create/rename policy. Generic slug validation, URN parsing and composition remain lenient so existing stored URNs and ID-shaped task leaves still round-trip. The obsolete org-root-not-dotted reason is retired.

License

MIT © Baragaun, Inc.

Documentation

Overview

Package urn is the Go implementation of the Hadron URN library, paired with @hadron-memory/urn-lib-js. Both run the SAME conformance corpus (fixtures/corpus.json) — the corpus is the contract, so the two cannot drift.

This is increment 1: the pure "scheme + registry + normalize + slug" slice, ported verbatim (v1 parity) from hadron-server/src/lib/urn.ts. parse/format/ compose and the grammar-v2 flat forms land in later increments.

Index

Constants

View Source
const (
	CanonicalScheme = "hrn"
	LegacyScheme    = "urn"
)

Scheme prefixes (issue #239). hrn: is canonical; legacy urn: is accepted on input forever. Every emission path composes hrn:.

View Source
const MaxAtomLen = 64

MaxAtomLen is the maximum length of a single URN atom / slug, in bytes (spec cor:urn:010:01, FR-017). Exported as the single source of truth so lib internals and server-side minters stop re-typing the bare literal 64 (#715).

Variables

View Source
var CoreTypes = []string{
	"agent", "app", "app-user", "ai-config", "asset", "edge",
	"license", "memory", "node", "org", "platform", "reference",
	"secret", "session", "subscription", "usage", "user",
}

CoreTypes are the core URN types.

View Source
var DisplayURNTypes = []string{"org", "memory", "agent", "app", "node", "user", "apprun", "worker", "ticker", "agenttask"}

DisplayURNTypes — the visible URN kinds rendered as a typed chip. Narrower than the canonical registry on purpose: a new visible kind must be added here too, or ParseDisplayUrn renders it as hrn:unknown:...

View Source
var NodeParts = []string{"condition", "data"}

NodeParts are the node parts.

View Source
var NodeRoles = []string{
	"chat", "chat-message", "config", "conversation", "event", "goal", "stage",
}

NodeRoles are the node roles.

View Source
var NodeTypes = []string{
	"abstract", "partial", "parent", "plan", "prompt", "record", "task", "review",
}

NodeTypes are the node types.

View Source
var RoleMarkers = []string{"system", "app-mem", "app-user", "group-mem", "priv", "anon"}

RoleMarkers are the memory-role markers (slug validation reserves their prefixes).

View Source
var TypeMarkers = map[string]bool{"app": true, "agent": true, "memory": true}

TypeMarkers are type-marker words that may appear inside path-segments.

View Source
var V2URNTypes = []string{
	"org", "user", "mem", "agent", "app", "node", "edge", "asset", "secret",
	"apprun", "worker", "ticker", "agenttask", "noderev", "appkey", "aiconf", "tool", "server", "userapikey",
	"agentschedule", "agentwebhook", "license", "subscription", "usage",
	"reference", "session", "platform",
}

V2URNTypes — the v2 type registry (SEEDED; finalized in #698). Note the renames vs v1 (memory->mem) and additions (apprun, noderev, appkey, ...); `data` is demoted to a #fragment and is NOT a type.

Functions

func AgentSlugFromUrn

func AgentSlugFromUrn(agentURN string) string

AgentSlugFromUrn extracts an agent's slug path, stripping the leading org segment. Robust to both legacy single-colon and canonical double-colon forms.

func AssertFullyQualifiedUrn

func AssertFullyQualifiedUrn(input, expectedType string) error

AssertFullyQualifiedUrn rejects inputs that lack the fully-qualified shape for expectedType. Checks SHAPE, not full canonical grammar.

func ComposeAgentTaskUrnV2 added in v0.0.17

func ComposeAgentTaskUrnV2(root, app, taskID string) (string, error)

ComposeAgentTaskUrnV2 composes hrn:agenttask:<root>:<app>:<task-id> using the task's id.

func ComposeAppRunUrnV2

func ComposeAppRunUrnV2(root, app, runID string) (string, error)

ComposeAppRunUrnV2 composes hrn:apprun:<root>:<app>:<run-id>.

func ComposeDataFragmentV2

func ComposeDataFragmentV2(parentURN string) (string, error)

ComposeDataFragmentV2 appends the #data fragment to a v2 node or apprun URN.

func ComposeEdgeUrn

func ComposeEdgeUrn(memURN, loc string) (string, error)

ComposeEdgeUrn builds a canonical hrn:edge:<mem>::<loc> URN.

func ComposeInstalledAgentUrn

func ComposeInstalledAgentUrn(appURN, agentURN string) (string, error)

ComposeInstalledAgentUrn composes the R2 canonical install URN for an Agent installed in an App: hrn:agent:<installing-org>::<app-slug>::<author-org>:<agent-slug>. Inputs MUST be 2-segment (org + slug) URNs. Install-by-self collapses via cat 1.

func ComposeNodeRevUrnV2

func ComposeNodeRevUrnV2(root, mem, loc, rev string) (string, error)

ComposeNodeRevUrnV2 composes hrn:noderev:<root>:<mem>:<loc...>:<rev>. `loc` may be a multi-atom node loc (colon-joined); it is split into its atoms so the terminal rev stays end-anchored.

func ComposeNodeUrn

func ComposeNodeUrn(memURN, loc string) (string, error)

ComposeNodeUrn builds a canonical hrn:node:<mem>::<loc> URN.

func ComposeSecretUrnV2

func ComposeSecretUrnV2(root, name string) (string, error)

ComposeSecretUrnV2 composes hrn:secret:<root>:<name> (org/user root + name).

func ComposeTickerUrnV2 added in v0.0.17

func ComposeTickerUrnV2(root, app, tickerSlug string) (string, error)

ComposeTickerUrnV2 composes hrn:ticker:<root>:<app>:<ticker-slug> using the permanent derived slug.

func ComposeUrnV2

func ComposeUrnV2(typ, root string, segments ...string) (string, error)

ComposeUrnV2 composes a canonical grammar-v2 flat URN.

func ComposeWorkerUrnV2

func ComposeWorkerUrnV2(root, app, workerSlug string) (string, error)

ComposeWorkerUrnV2 composes hrn:worker:<root>:<app>:<worker-slug> — a Worker, the named casting of an Agent into an App (cor:dmo:050:11). The leaf atom is the Worker's stored `slug` column, DERIVED from its name at cast time and iterated to uniqueness; it is never the display name, which carries no charset constraint and may collide after slugification (cor:agt:020:02).

func DeriveSlugFromName

func DeriveSlugFromName(name string) (string, error)

DeriveSlugFromName derives a valid, lowercase slug atom from a free-form name. Slugification only — it does NOT reject reserved words. Returns ReasonEmptyDerivedSlug when nothing usable survives.

func FormatCanonicalUrn

func FormatCanonicalUrn(typ, bareValue string) (string, error)

FormatCanonicalUrn composes and canonicalizes a full URN from an entity type and a bare-stored value (legacy single-colon or canonical :: hierarchy). Always returns the canonical hrn:<type>: form.

func FormatUrn

func FormatUrn(typ, value string) string

FormatUrn formats a bare value as a typed canonical URN string.

func HasSchemePrefix

func HasSchemePrefix(input string) bool

HasSchemePrefix reports whether input leads with a scheme prefix.

func IsFlatV2

func IsFlatV2(input string) bool

IsFlatV2 reports whether input is already a canonical grammar-v2 flat URN.

func IsParserCanonical

func IsParserCanonical(input string) bool

IsParserCanonical reports whether ParseUrn(input).ParserCanonical == input with no rewrites.

func IsValidSlug

func IsValidSlug(slug string) bool

IsValidSlug is the boolean form of ValidateUserSlug: true when slug is a legal NEW entity slug (charset + length + reserved-word + lowercase), false on any violation. Convenience predicate for callers that don't want an error (#715).

func LegacyMemoryUrnToCanonical

func LegacyMemoryUrnToCanonical(bareLegacyURN string) string

LegacyMemoryUrnToCanonical converts a bare legacy single-colon memory URN to canonical :: hierarchy, preserving the marker:id boundary for role-marker memories. Input MUST be the bare legacy form.

func NormalizeScheme

func NormalizeScheme(input string) string

NormalizeScheme rewrites a leading legacy urn: scheme to canonical hrn:. Bare and already-canonical inputs pass through.

func NormalizeUrnForLookup

func NormalizeUrnForLookup(bareURN string) string

NormalizeUrnForLookup collapses the canonical :: hierarchy form to the bare single-colon lookup form.

func ToParserCanonical

func ToParserCanonical(input string) (string, error)

ToParserCanonical parses and returns the parser-layer canonical form.

func ValidateAtomShape

func ValidateAtomShape(input, atom string) error

ValidateAtomShape checks the FR-016 charset + FR-017 length only. Deliberately case-lenient (the read/parse path); the lowercase rule is enforced at create in ValidateUserSlug.

func ValidateOrgSlug

func ValidateOrgSlug(slug string) error

ValidateOrgSlug validates a new or renamed org root: bare, legal lowercase atom, neither reserved nor ID-shaped. Dotted and undotted names are admitted. Existing URNs remain readable through the lenient parse path.

func ValidateUrnType

func ValidateUrnType(parsed LegacyParsedURN, expected string) *string

ValidateUrnType returns nil if the parsed type matches expected (with the unknown-always-ok and loc-where-node-expected exceptions), otherwise a pointer to the mismatch message. A *string (nil = ok) mirrors the JS `string | null`.

func ValidateUserHandle

func ValidateUserHandle(handle string) error

ValidateUserHandle validates a new handle: root policy plus the existing dot-free rule.

func ValidateUserSlug

func ValidateUserSlug(slug string) error

ValidateUserSlug validates a slug at create/rename: charset + length + reserved-word rejection + the lowercase-canonical rule (#575).

Types

type LegacyParsedURN

type LegacyParsedURN struct {
	Type  string `json:"type"`
	Value string `json:"value"`
}

LegacyParsedURN is the result of ParseUrnInput. JSON tags match the corpus.

func ParseUrnInput

func ParseUrnInput(input string) LegacyParsedURN

ParseUrnInput strips the hrn:/urn:<type>: or loc: prefix and returns the type and bare value. The v2 `mem` type word is normalized to `memory`. Unprefixed inputs are returned as type "unknown".

type NodeLikeURNParts added in v0.0.17

type NodeLikeURNParts struct {
	// MemoryURN is the bare <org>:<memorySlug>, or the full path for a
	// multi-segment memory (<org>:<app>:<agent>:<role>).
	MemoryURN string `json:"memoryUrn"`
	Loc       string `json:"loc"`
	// Fragment is set only when the input carried a #<fragment> suffix (v2
	// #data). Omitted otherwise so existing corpus cases keep matching.
	Fragment string `json:"fragment,omitempty"`
}

NodeLikeURNParts is the result of SplitNodeUrn / SplitEdgeUrn (node and edge share one shape). JSON tags match the corpus.

Loc is GRAMMAR-NORMALIZED: both hrn:node:acme.com::specs::cor:urn and hrn:node:acme.com:specs:cor:urn yield "cor:urn", so a caller never has to know which grammar it was handed. That is the reason to prefer these decomposers over ParsedURN.PathSegments, which is a RAW split whose shape follows the input grammar (urn-lib-js#12).

func SplitEdgeUrn added in v0.0.17

func SplitEdgeUrn(input string) (NodeLikeURNParts, error)

SplitEdgeUrn splits a fully-qualified edge URN into its memory URN and loc (urn-lib-js#12). Same shape as SplitNodeUrn — the edge loc is an OPAQUE terminal and is never re-split into source:target.

func SplitNodeUrn

func SplitNodeUrn(input string) (NodeLikeURNParts, error)

SplitNodeUrn splits a fully-qualified node URN into its memory URN and loc. Self-validating (AssertFullyQualifiedUrn(input, "node")). A #data fragment is reported separately rather than folded into Loc (urn-lib-js#13).

type NotQualifiedError

type NotQualifiedError struct {
	OffendingValue string
	ExpectedType   string
}

NotQualifiedError is returned when a non-ID-shaped input fails URN qualification. Code() returns the stable cross-language contract handle.

func (*NotQualifiedError) Code

func (e *NotQualifiedError) Code() string

Code is the stable contract handle (mirrors urn-lib-js UrnNotQualifiedError.code).

func (*NotQualifiedError) Error

func (e *NotQualifiedError) Error() string

type ParseError

type ParseError struct {
	Input            string
	Reason           Reason
	OffendingSegment string
}

ParseError is returned by every validation/parse entry point on any acceptance violation.

func (*ParseError) Error

func (e *ParseError) Error() string

type ParseForResult

type ParseForResult struct {
	Canonical string `json:"canonical"`
	IsLegacy  bool   `json:"isLegacy"`
}

ParseForResult is the ParseFor envelope. JSON tags match the corpus.

func ParseFor

func ParseFor(row UrnRow) (ParseForResult, error)

ParseFor dispatches per-row: a normalized row is parsed canonically and its stored URN returned unchanged (drift throws; scheme differences tolerated); a legacy row is validated with the legacy parser (unknown shape throws).

type ParsedDisplayURN

type ParsedDisplayURN struct {
	Type      string `json:"type"`
	BareValue string `json:"bareValue"`
	FullURN   string `json:"fullUrn"`
}

ParsedDisplayURN is the result of ParseDisplayUrn. JSON tags match the corpus.

func ParseDisplayUrn

func ParseDisplayUrn(value, typeHint string) ParsedDisplayURN

ParseDisplayUrn parses a displayable URN for chip rendering. `typeHint` is "" for none. Auto-detects a scheme-prefixed display kind; else applies the hint to a BARE value only; else falls back to "unknown".

type ParsedNodeRevURNV2

type ParsedNodeRevURNV2 struct {
	Scheme string `json:"scheme"`
	Type   string `json:"type"`
	Root   string `json:"root"`
	Memory string `json:"memory"`
	Loc    string `json:"loc"`
	Rev    string `json:"rev"`
}

ParsedNodeRevURNV2 is the END-ANCHORED decomposition of a hrn:noderev URN.

func ParseNodeRevUrnV2

func ParseNodeRevUrnV2(input string) (ParsedNodeRevURNV2, error)

ParseNodeRevUrnV2 parses and END-ANCHORED-decomposes a hrn:noderev URN (#696). The LAST atom is the revision id; the FIRST post-root atom is the memory slug; everything between is the variable-length, opaque node loc.

type ParsedURN

type ParsedURN struct {
	Type                          string   `json:"type"`
	PathSegments                  []string `json:"pathSegments"`
	ParserCanonical               string   `json:"parserCanonical"`
	InputForm                     string   `json:"inputForm"`
	ParserRewrites                []string `json:"parserRewrites"`
	NeedsResolverCanonicalization bool     `json:"needsResolverCanonicalization"`
}

ParsedURN is the result of ParseUrn. JSON tags match the conformance corpus.

func ParseUrn

func ParseUrn(input string) (ParsedURN, error)

ParseUrn parses a URN string, applying parser-layer canonicalization (D11 cats 1, 4). Returns a *ParseError on any acceptance violation. The v1 grammar is tried first; a v1-rejected input that is a valid flat grammar-v2 URN (single-colon, pool-rooted, `mem` type word — #697) is delegated to ParseUrnV2 and mapped into the ParsedURN shape. v1-accepted inputs keep their exact v1 result, so no existing behavior changes.

type ParsedURNV2

type ParsedURNV2 struct {
	Scheme   string   `json:"scheme"`
	Type     string   `json:"type"`
	Root     string   `json:"root"`
	Segments []string `json:"segments"`
	// Fragment is present only when the URN carried a #<fragment> suffix.
	Fragment string `json:"fragment,omitempty"`
}

ParsedURNV2 is the result of ParseUrnV2. JSON tags match the corpus.

func ParseUrnV2

func ParseUrnV2(input string) (ParsedURNV2, error)

ParseUrnV2 parses a STRICT grammar-v2 flat URN. Rejects v1 constructs (the :: hierarchy separator and the @/user: root sigil). Enforces the #696 per-entity arity and the #data fragment rules.

type Reason

type Reason string

Reason is a machine-stable failure code. These ARE part of the fixture contract (shared with urn-lib-js); the human message text is not.

const (
	ReasonUnknownType         Reason = "unknown-type"
	ReasonMalformedGrammar    Reason = "malformed-grammar"
	ReasonEmptySegment        Reason = "empty-segment"
	ReasonTrailingDoubleColon Reason = "trailing-double-colon"
	ReasonReservedWordSlug    Reason = "reserved-word-slug"
	ReasonInvalidCharset      Reason = "invalid-charset"
	ReasonSlugNotLowercase    Reason = "slug-not-lowercase"
	ReasonSlugTooLong         Reason = "slug-too-long"
	ReasonLocSegmentRejected  Reason = "loc-segment-rejected"
	ReasonInvalidSegmentShape Reason = "invalid-segment-shape"
	// ReasonFragmentUnsupported — the URN is valid apart from its #<fragment>
	// (urn-lib-js#11). Only "#data" is registered, and only a node/apprun URN may
	// parent it; a v1-grammar URN carries no fragment at all.
	ReasonFragmentUnsupported  Reason = "fragment-unsupported"
	ReasonEmptyBareValue       Reason = "empty-bare-value"
	ReasonAlreadyPrefixedValue Reason = "already-prefixed-bare-value"
	ReasonOrgUrnNotBare        Reason = "org-urn-not-bare"
	ReasonRootIDShaped         Reason = "root-id-shaped"
	ReasonHandleHasDot         Reason = "handle-has-dot"
	ReasonEmptyDerivedSlug     Reason = "empty-derived-slug"
)

type UrnRow

type UrnRow struct {
	URN             string
	URNNormalizedAt *time.Time
}

UrnRow is the minimal row shape for ParseFor. A nil URNNormalizedAt means the row is pre-normalization (legacy shape).

Jump to

Keyboard shortcuts

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