From 5e9f61acf41b155e2128d32f07fd1a422175f297 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Feb 2026 20:43:59 +0000 Subject: [PATCH] feat(manifest): add .core/view.yml types and parser Manifest struct, Permissions, Parse() from YAML, SlotNames() helper. Foundation for Phase 4 module system. Co-Authored-By: Claude Opus 4.6 --- pkg/manifest/manifest.go | 50 +++++++++++++++++++++++++++ pkg/manifest/manifest_test.go | 65 +++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 pkg/manifest/manifest.go create mode 100644 pkg/manifest/manifest_test.go diff --git a/pkg/manifest/manifest.go b/pkg/manifest/manifest.go new file mode 100644 index 0000000..72ae785 --- /dev/null +++ b/pkg/manifest/manifest.go @@ -0,0 +1,50 @@ +package manifest + +import ( + "fmt" + + "gopkg.in/yaml.v3" +) + +// Manifest represents a .core/view.yml application manifest. +type Manifest struct { + Code string `yaml:"code"` + Name string `yaml:"name"` + Version string `yaml:"version"` + Sign string `yaml:"sign"` + Layout string `yaml:"layout"` + Slots map[string]string `yaml:"slots"` + + Permissions Permissions `yaml:"permissions"` + Modules []string `yaml:"modules"` +} + +// Permissions declares the I/O capabilities a module requires. +type Permissions struct { + Read []string `yaml:"read"` + Write []string `yaml:"write"` + Net []string `yaml:"net"` + Run []string `yaml:"run"` +} + +// Parse decodes YAML bytes into a Manifest. +func Parse(data []byte) (*Manifest, error) { + var m Manifest + if err := yaml.Unmarshal(data, &m); err != nil { + return nil, fmt.Errorf("manifest.Parse: %w", err) + } + return &m, nil +} + +// SlotNames returns a deduplicated list of component names from slots. +func (m *Manifest) SlotNames() []string { + seen := make(map[string]bool) + var names []string + for _, name := range m.Slots { + if !seen[name] { + seen[name] = true + names = append(names, name) + } + } + return names +} diff --git a/pkg/manifest/manifest_test.go b/pkg/manifest/manifest_test.go new file mode 100644 index 0000000..63ca253 --- /dev/null +++ b/pkg/manifest/manifest_test.go @@ -0,0 +1,65 @@ +package manifest + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestParse_Good(t *testing.T) { + raw := ` +code: photo-browser +name: Photo Browser +version: 0.1.0 +sign: dGVzdHNpZw== + +layout: HLCRF +slots: + H: nav-breadcrumb + L: folder-tree + C: photo-grid + R: metadata-panel + F: status-bar + +permissions: + read: ["./photos/"] + write: [] + net: [] + run: [] + +modules: + - core/media + - core/fs +` + m, err := Parse([]byte(raw)) + require.NoError(t, err) + assert.Equal(t, "photo-browser", m.Code) + assert.Equal(t, "Photo Browser", m.Name) + assert.Equal(t, "0.1.0", m.Version) + assert.Equal(t, "dGVzdHNpZw==", m.Sign) + assert.Equal(t, "HLCRF", m.Layout) + assert.Equal(t, "nav-breadcrumb", m.Slots["H"]) + assert.Equal(t, "photo-grid", m.Slots["C"]) + assert.Len(t, m.Permissions.Read, 1) + assert.Equal(t, "./photos/", m.Permissions.Read[0]) + assert.Len(t, m.Modules, 2) +} + +func TestParse_Bad(t *testing.T) { + _, err := Parse([]byte("not: valid: yaml: [")) + assert.Error(t, err) +} + +func TestManifest_SlotNames_Good(t *testing.T) { + m := Manifest{ + Slots: map[string]string{ + "H": "nav-bar", + "C": "main-content", + }, + } + names := m.SlotNames() + assert.Contains(t, names, "nav-bar") + assert.Contains(t, names, "main-content") + assert.Len(t, names, 2) +}