agent endpoints: key templates by shape and export their ambiguity

Canonical renders a template as its literals and convertor kinds, so two
templates the trie cannot tell apart share a key. Param names reach the trie
only as a kind, so /x/{a} and /x/{b} accept the same paths; anything keying
routes by the declared spelling splits them apart. Literals are length-prefixed
because a brace that opens no well-formed param is literal text: /x/{:str}
matches itself and must not collide with /x/{a}.

ambiguousTemplate becomes Template.Ambiguous. It reads only the template's own
elements, so it answers for a manifest that was validated but never compiled
into a table of its own.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Paul Wells
2026-09-16 07:26:25 -07:00
co-authored by Claude Opus 5
parent b567fcfa78
commit cb8f640865
2 changed files with 42 additions and 19 deletions
+1 -19
View File
@@ -54,7 +54,7 @@ func (b *Builder[T]) Add(t *Template, m Mask, v T) error {
idx := b.n
b.n++
if ambiguousTemplate(t) {
if t.Ambiguous() {
b.ambiguous = append(b.ambiguous, t.raw)
}
@@ -125,24 +125,6 @@ func singleRun[T any](e *buildEdge[T]) bool {
return true
}
// ambiguousTemplate reports whether a template's own shape can force a search,
// independent of what other templates put in the tree.
func ambiguousTemplate(t *Template) bool {
for i, e := range t.elements {
if e.kind == kindLiteral || e.kind == kindUUID {
continue
}
if i == len(t.elements)-1 {
continue // terminal: only the greedy run can reach the end
}
next := t.elements[i+1]
if next.kind != kindLiteral || e.kind.charset(next.lit[0]) {
return true
}
}
return false
}
func insertLiteral[T any](n *buildNode[T], lit string, idx uint32) *buildNode[T] {
for len(lit) > 0 {
e := literalEdge(n, lit[0])
+41
View File
@@ -16,6 +16,7 @@ package router
import (
"fmt"
"strconv"
"strings"
"unicode/utf8"
)
@@ -67,6 +68,46 @@ type Template struct {
// String returns the template as declared.
func (t *Template) String() string { return t.raw }
// Canonical returns a key identifying what the template matches: two templates
// with the same canonical form accept exactly the same paths and differ only in
// param names. Literals are length-prefixed so that a literal brace cannot forge
// a convertor - "/x/{:str}" is literal text, and must not collide with "/x/{a}".
func (t *Template) Canonical() string {
var b strings.Builder
for _, e := range t.elements {
if e.kind == kindLiteral {
b.WriteByte('L')
b.WriteString(strconv.Itoa(len(e.lit)))
b.WriteByte(':')
b.WriteString(e.lit)
continue
}
b.WriteByte('K')
b.WriteString(strconv.Itoa(int(e.kind)))
}
return b.String()
}
// Ambiguous reports whether the template's own shape can force the matcher to
// backtrack - a param a following literal can extend, adjacent params, or a
// non-final path convertor - independent of what other templates put in the
// tree.
func (t *Template) Ambiguous() bool {
for i, e := range t.elements {
if e.kind == kindLiteral || e.kind == kindUUID {
continue
}
if i == len(t.elements)-1 {
continue // terminal: only the greedy run can reach the end
}
next := t.elements[i+1]
if next.kind != kindLiteral || e.kind.charset(next.lit[0]) {
return true
}
}
return false
}
// ParseTemplate parses a starlette path template. Custom convertors are
// rejected: only the five built-ins may travel over the wire.
func ParseTemplate(path string) (*Template, error) {