package core import ( "fmt" "strings" ) const ( // MaxDocIDPrefixLen bounds the alphabetic part of an ID ("SPEC", "RFC"). MaxDocIDPrefixLen = 16 // MaxDocIDSeqLen bounds the numeric part. Eight digits is far past the // volume this service will ever see; the cap exists so a pathological ID // cannot become a pathological registry key. MaxDocIDSeqLen = 8 ) // DocID is a parsed document identifier such as "SPEC-0007". // // Seq is kept as a string rather than an int on purpose: leading zeros are part // of how these IDs are written and read, and "SPEC-7" and "SPEC-0007" are // therefore different IDs in a registry that is globally unique. Normalizing // them to the same integer would silently merge two documents. type DocID struct { Prefix string Seq string } // String renders the canonical "PREFIX-SEQ" form. func (d DocID) String() string { return d.Prefix + "-" + d.Seq } // ParseDocID parses and validates a document ID. The grammar is deliberately // tiny: // // ID = PREFIX "-" SEQ // PREFIX = [A-Z] [A-Z0-9]* (1..MaxDocIDPrefixLen) // SEQ = [0-9]+ (1..MaxDocIDSeqLen) // // Exactly one '-' separates the two, so the split is unambiguous and IDs sort // predictably. ASCII-only and uppercase-only is the load-bearing part: IDs are // the global registry key, so a Cyrillic "ะก" or a lowercase "spec" must be a // different-looking ID that is rejected outright rather than a homograph that // quietly registers alongside the real one. // // Core validates shape only. Global uniqueness is enforced by the registry // table, which is the only thing that can know about other spaces. func ParseDocID(s string) (DocID, error) { if s == "" { return DocID{}, fmt.Errorf("%w: empty id", ErrInvalidDocID) } i := strings.IndexByte(s, '-') if i < 0 { return DocID{}, fmt.Errorf("%w: id %q has no '-' separator", ErrInvalidDocID, s) } prefix, seq := s[:i], s[i+1:] if strings.IndexByte(seq, '-') >= 0 { return DocID{}, fmt.Errorf("%w: id %q has more than one '-'", ErrInvalidDocID, s) } if prefix == "" { return DocID{}, fmt.Errorf("%w: id %q has an empty prefix", ErrInvalidDocID, s) } if len(prefix) > MaxDocIDPrefixLen { return DocID{}, fmt.Errorf("%w: id %q prefix is too long (%d > %d)", ErrInvalidDocID, s, len(prefix), MaxDocIDPrefixLen) } if !isUpperAlpha(prefix[0]) { return DocID{}, fmt.Errorf("%w: id %q prefix must start with A-Z", ErrInvalidDocID, s) } for i := 0; i < len(prefix); i++ { if c := prefix[i]; !isUpperAlpha(c) && !isDigit(c) { return DocID{}, fmt.Errorf("%w: id %q prefix contains disallowed byte %q", ErrInvalidDocID, s, c) } } if seq == "" { return DocID{}, fmt.Errorf("%w: id %q has an empty sequence", ErrInvalidDocID, s) } if len(seq) > MaxDocIDSeqLen { return DocID{}, fmt.Errorf("%w: id %q sequence is too long (%d > %d)", ErrInvalidDocID, s, len(seq), MaxDocIDSeqLen) } for i := 0; i < len(seq); i++ { if !isDigit(seq[i]) { return DocID{}, fmt.Errorf("%w: id %q sequence contains disallowed byte %q", ErrInvalidDocID, s, seq[i]) } } return DocID{Prefix: prefix, Seq: seq}, nil } // ValidateDocID reports whether s is a well-formed document ID, discarding the // parse. Use it where only the verdict matters (frontmatter schema checks, push // validation). func ValidateDocID(s string) error { _, err := ParseDocID(s) return err } func isUpperAlpha(c byte) bool { return c >= 'A' && c <= 'Z' } func isDigit(c byte) bool { return c >= '0' && c <= '9' }