Add Phase 1 foundation: Go module, core types, DB infrastructure, config
Establish the project foundation with three packages: - core: shared types (Header, Metadata, Value, ObjectType, UUID generation) - db: SQLite migration framework, connection management (WAL, FK, busy timeout), transaction helpers (StartTX/EndTX), time conversion - config: runtime configuration (DB path, blob store, Minio, gRPC addr) Includes initial schema migration (001_initial.sql) with 13 tables covering shared infrastructure, bibliographic data, and artifact repository. Full test coverage for all packages, strict linting (.golangci.yaml), and Makefile. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
111
core/core.go
Normal file
111
core/core.go
Normal file
@@ -0,0 +1,111 @@
|
||||
// Package core defines shared types used across both the artifact repository
|
||||
// and knowledge graph pillars.
|
||||
package core
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// ObjectType enumerates the kinds of persistent objects in the system.
|
||||
type ObjectType string
|
||||
|
||||
const (
|
||||
ObjectTypeArtifact ObjectType = "artifact"
|
||||
ObjectTypeSnapshot ObjectType = "snapshot"
|
||||
ObjectTypeCitation ObjectType = "citation"
|
||||
ObjectTypePublisher ObjectType = "publisher"
|
||||
ObjectTypeNode ObjectType = "node"
|
||||
ObjectTypeCell ObjectType = "cell"
|
||||
)
|
||||
|
||||
// Header is attached to every persistent object.
|
||||
type Header struct {
|
||||
Meta Metadata
|
||||
Categories []string
|
||||
Tags []string
|
||||
ID string
|
||||
Type ObjectType
|
||||
Created int64
|
||||
Modified int64
|
||||
}
|
||||
|
||||
// NewHeader creates a Header with a new UUID and current timestamps.
|
||||
func NewHeader(objType ObjectType) Header {
|
||||
now := time.Now().UTC().Unix()
|
||||
return Header{
|
||||
ID: NewUUID(),
|
||||
Type: objType,
|
||||
Created: now,
|
||||
Modified: now,
|
||||
Meta: Metadata{},
|
||||
}
|
||||
}
|
||||
|
||||
// Touch updates the Modified timestamp to now.
|
||||
func (h *Header) Touch() {
|
||||
h.Modified = time.Now().UTC().Unix()
|
||||
}
|
||||
|
||||
const (
|
||||
// ValueTypeUnspecified is used when the value type hasn't been explicitly
|
||||
// set. It should be interpreted as a string.
|
||||
ValueTypeUnspecified = "UNSPECIFIED"
|
||||
|
||||
// ValueTypeString is used when the value type should be explicitly
|
||||
// interpreted as a string.
|
||||
ValueTypeString = "String"
|
||||
|
||||
// ValueTypeInt is used when the value should be interpreted as an integer.
|
||||
ValueTypeInt = "Int"
|
||||
)
|
||||
|
||||
// Value stores a typed key-value entry. Contents is always stored as a string;
|
||||
// the Type field tells consumers how to interpret it.
|
||||
type Value struct {
|
||||
Contents string
|
||||
Type string
|
||||
}
|
||||
|
||||
// Val creates a new Value with an unspecified type.
|
||||
func Val(contents string) Value {
|
||||
return Value{Contents: contents, Type: ValueTypeUnspecified}
|
||||
}
|
||||
|
||||
// Vals creates a new Value with a string type.
|
||||
func Vals(contents string) Value {
|
||||
return Value{Contents: contents, Type: ValueTypeString}
|
||||
}
|
||||
|
||||
// Metadata holds additional information that isn't explicitly part of a data
|
||||
// definition. Keys are arbitrary strings; values carry type information.
|
||||
type Metadata map[string]Value
|
||||
|
||||
// NewUUID returns a new random UUID string.
|
||||
func NewUUID() string {
|
||||
return uuid.NewString()
|
||||
}
|
||||
|
||||
// ErrNoID is returned when a lookup is done on a struct that has no identifier.
|
||||
var ErrNoID = errors.New("missing UUID identifier")
|
||||
|
||||
// MapFromList converts a string slice into a map[string]bool for set-like
|
||||
// membership testing.
|
||||
func MapFromList(list []string) map[string]bool {
|
||||
m := make(map[string]bool, len(list))
|
||||
for _, s := range list {
|
||||
m[s] = true
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// ListFromMap converts a map[string]bool (set) back to a sorted slice.
|
||||
func ListFromMap(m map[string]bool) []string {
|
||||
list := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
list = append(list, k)
|
||||
}
|
||||
return list
|
||||
}
|
||||
106
core/core_test.go
Normal file
106
core/core_test.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNewUUID(t *testing.T) {
|
||||
id1 := NewUUID()
|
||||
id2 := NewUUID()
|
||||
if id1 == "" {
|
||||
t.Fatal("NewUUID returned empty string")
|
||||
}
|
||||
if id1 == id2 {
|
||||
t.Fatal("NewUUID returned duplicate UUIDs")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVal(t *testing.T) {
|
||||
v := Val("hello")
|
||||
if v.Contents != "hello" {
|
||||
t.Fatalf("expected contents 'hello', got %q", v.Contents)
|
||||
}
|
||||
if v.Type != ValueTypeUnspecified {
|
||||
t.Fatalf("expected type %q, got %q", ValueTypeUnspecified, v.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVals(t *testing.T) {
|
||||
v := Vals("world")
|
||||
if v.Contents != "world" {
|
||||
t.Fatalf("expected contents 'world', got %q", v.Contents)
|
||||
}
|
||||
if v.Type != ValueTypeString {
|
||||
t.Fatalf("expected type %q, got %q", ValueTypeString, v.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewHeader(t *testing.T) {
|
||||
before := time.Now().UTC().Unix()
|
||||
h := NewHeader(ObjectTypeArtifact)
|
||||
after := time.Now().UTC().Unix()
|
||||
|
||||
if h.ID == "" {
|
||||
t.Fatal("header ID is empty")
|
||||
}
|
||||
if h.Type != ObjectTypeArtifact {
|
||||
t.Fatalf("expected type %q, got %q", ObjectTypeArtifact, h.Type)
|
||||
}
|
||||
if h.Created < before || h.Created > after {
|
||||
t.Fatalf("Created timestamp %d not in range [%d, %d]", h.Created, before, after)
|
||||
}
|
||||
if h.Modified != h.Created {
|
||||
t.Fatalf("Modified (%d) should equal Created (%d)", h.Modified, h.Created)
|
||||
}
|
||||
if h.Meta == nil {
|
||||
t.Fatal("Meta should not be nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeaderTouch(t *testing.T) {
|
||||
h := NewHeader(ObjectTypeNode)
|
||||
original := h.Modified
|
||||
// Ensure at least 1 second passes for timestamp change.
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
h.Touch()
|
||||
if h.Modified < original {
|
||||
t.Fatalf("Touch should not decrease Modified: was %d, now %d", original, h.Modified)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapFromList(t *testing.T) {
|
||||
list := []string{"alpha", "beta", "gamma"}
|
||||
m := MapFromList(list)
|
||||
if len(m) != 3 {
|
||||
t.Fatalf("expected 3 entries, got %d", len(m))
|
||||
}
|
||||
for _, s := range list {
|
||||
if !m[s] {
|
||||
t.Fatalf("expected %q in map", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestListFromMap(t *testing.T) {
|
||||
m := map[string]bool{"z": true, "a": true, "m": true}
|
||||
list := ListFromMap(m)
|
||||
if len(list) != 3 {
|
||||
t.Fatalf("expected 3 entries, got %d", len(list))
|
||||
}
|
||||
sort.Strings(list)
|
||||
expected := []string{"a", "m", "z"}
|
||||
for i, v := range list {
|
||||
if v != expected[i] {
|
||||
t.Fatalf("index %d: expected %q, got %q", i, expected[i], v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapFromListEmpty(t *testing.T) {
|
||||
m := MapFromList(nil)
|
||||
if len(m) != 0 {
|
||||
t.Fatalf("expected empty map, got %d entries", len(m))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user