diff --git a/zddc/internal/zddc/defaults.go b/zddc/internal/zddc/defaults.go index 1b1a201..d06ff69 100644 --- a/zddc/internal/zddc/defaults.go +++ b/zddc/internal/zddc/defaults.go @@ -1,40 +1,68 @@ package zddc import ( - _ "embed" + "embed" "sync" ) -// defaultsBytes is the embedded baseline .zddc — see defaults.zddc.yaml -// for the source-of-truth and a description of its role in the cascade. +// defaultsBytes is the legacy single-file embedded baseline. Retained only so +// TestEmbeddedTreeMatchesYAML can prove the per-depth tree (the new source of +// truth) assembles to exactly the same ZddcFile. Removed once that guarantee +// is locked. (Still surfaced by EmbeddedDefaultsBytes / show-defaults for now.) // //go:embed defaults.zddc.yaml var defaultsBytes []byte -// EmbeddedDefaultsBytes returns the raw embedded defaults YAML. +// defaultsTreeFS is the embedded per-depth default policy tree — the source of +// truth. `all:` includes the `.zddc` (dot) files and `_any_` (underscore) +// directories that a bare //go:embed would skip. // -// Surface: the show-defaults CLI subcommand dumps these bytes to -// stdout so operators can copy them into /.zddc and edit. +//go:embed all:defaults +var defaultsTreeFS embed.FS + +// EmbeddedDefaultsBytes returns the raw embedded defaults YAML. Surface: the +// show-defaults CLI dumps these to stdout. func EmbeddedDefaultsBytes() []byte { out := make([]byte, len(defaultsBytes)) copy(out, defaultsBytes) return out } +var ( + embeddedTreeOnce sync.Once + embeddedTree PolicyTree + embeddedTreeErr error +) + +// EmbeddedPolicyTree returns the baked-in per-depth default policy tree, +// memoised. This is the embedded form of the .zddc.zip mounted at the +// deployment root (the bottom of every cascade). +func EmbeddedPolicyTree() (PolicyTree, error) { + embeddedTreeOnce.Do(func() { + embeddedTree, embeddedTreeErr = LoadPolicyTreeFromFS(defaultsTreeFS, "defaults") + }) + return embeddedTree, embeddedTreeErr +} + var ( embeddedDefaultsOnce sync.Once embeddedDefaults ZddcFile embeddedDefaultsErr error ) -// EmbeddedDefaults returns the parsed embedded defaults ZddcFile, -// memoised. Parse errors surface on the first call and are sticky. +// EmbeddedDefaults returns the embedded defaults assembled from the per-depth +// tree into the single nested ZddcFile the cascade walker consumes, memoised. // -// The cascade walker (EffectivePolicy) consults this as the bottom- -// most level unless an on-disk .zddc up the chain sets `inherit: false`. +// The cascade walker (EffectivePolicy) consults this as the bottom-most level +// unless an on-disk .zddc up the chain sets `inherit: false`. func EmbeddedDefaults() (ZddcFile, error) { embeddedDefaultsOnce.Do(func() { - embeddedDefaults, embeddedDefaultsErr = parseBytes(defaultsBytes) + tree, err := EmbeddedPolicyTree() + if err != nil { + embeddedDefaultsErr = err + return + } + embeddedDefaults = tree.Assemble() }) return embeddedDefaults, embeddedDefaultsErr } diff --git a/zddc/internal/zddc/zippolicy.go b/zddc/internal/zddc/zippolicy.go index 1f035bb..c9be3db 100644 --- a/zddc/internal/zddc/zippolicy.go +++ b/zddc/internal/zddc/zippolicy.go @@ -3,7 +3,7 @@ package zddc import ( "io/fs" "os" - "path/filepath" + "path" "strings" ) @@ -88,25 +88,58 @@ func (t PolicyTree) Along(relSegs []string) []ZddcFile { return out } -// LoadPolicyTreeFromDir loads a per-depth .zddc tree from fsDir, mapping the -// AnyPlaceholder directory to the "*" wildcard. Keys are member dirs relative -// to fsDir ("" for fsDir/.zddc). Used for the embedded default-tree source. -func LoadPolicyTreeFromDir(fsDir string) (PolicyTree, error) { +// Assemble folds a flat per-depth tree into a single nested ZddcFile whose +// Paths map mirrors the tree — the inverse of authoring policy as per-depth +// files. The result is exactly what a single nested-paths: .zddc would parse +// to, so the cascade walker consumes it unchanged: the embedded defaults and +// any operator .zddc.zip both become ordinary paths:-bearing ZddcFiles. +func (t PolicyTree) Assemble() ZddcFile { + return assembleTree(map[string]ZddcFile(t)) +} + +// assembleTree recursively builds the nested node for a set of members keyed by +// path relative to this node ("" = this node's own content; "head/rest" = a +// descendant). Children are grouped by first segment and recursed into Paths. +func assembleTree(members map[string]ZddcFile) ZddcFile { + node := members[""] + groups := map[string]map[string]ZddcFile{} + for key, zf := range members { + if key == "" { + continue + } + head, rest, _ := strings.Cut(key, "/") + if groups[head] == nil { + groups[head] = map[string]ZddcFile{} + } + groups[head][rest] = zf + } + if len(groups) > 0 { + node.Paths = make(map[string]ZddcFile, len(groups)) + for head, sub := range groups { + node.Paths[head] = assembleTree(sub) + } + } + return node +} + +// LoadPolicyTreeFromFS loads a per-depth .zddc tree rooted at `root` within +// fsys, mapping the AnyPlaceholder directory to the "*" wildcard. Keys are +// member dirs relative to root ("" for root/.zddc). Works on an embed.FS, an +// os.DirFS, or a zip's fs.FS. +func LoadPolicyTreeFromFS(fsys fs.FS, root string) (PolicyTree, error) { out := PolicyTree{} - err := filepath.WalkDir(fsDir, func(p string, d fs.DirEntry, walkErr error) error { + err := fs.WalkDir(fsys, root, func(p string, d fs.DirEntry, walkErr error) error { if walkErr != nil { return walkErr } if d.IsDir() || d.Name() != ".zddc" { return nil } - rel, err := filepath.Rel(fsDir, filepath.Dir(p)) - if err != nil { - return err - } + rel := strings.TrimPrefix(path.Dir(p), root) + rel = strings.Trim(rel, "/") key := "" - if rel != "." { - parts := strings.Split(filepath.ToSlash(rel), "/") + if rel != "" { + parts := strings.Split(rel, "/") for i, s := range parts { if s == AnyPlaceholder { parts[i] = "*" @@ -114,7 +147,7 @@ func LoadPolicyTreeFromDir(fsDir string) (PolicyTree, error) { } key = strings.Join(parts, "/") } - data, err := os.ReadFile(p) + data, err := fs.ReadFile(fsys, p) if err != nil { return err } @@ -130,3 +163,8 @@ func LoadPolicyTreeFromDir(fsDir string) (PolicyTree, error) { } return out, nil } + +// LoadPolicyTreeFromDir is the on-disk convenience over LoadPolicyTreeFromFS. +func LoadPolicyTreeFromDir(fsDir string) (PolicyTree, error) { + return LoadPolicyTreeFromFS(os.DirFS(fsDir), ".") +} diff --git a/zddc/internal/zddc/zippolicy_test.go b/zddc/internal/zddc/zippolicy_test.go index cc6a181..a98e002 100644 --- a/zddc/internal/zddc/zippolicy_test.go +++ b/zddc/internal/zddc/zippolicy_test.go @@ -119,6 +119,24 @@ func TestPolicyTreeAlong(t *testing.T) { } } +// Phase-4 gate: the embedded per-depth tree assembles to EXACTLY the legacy +// defaults.zddc.yaml ZddcFile, so pointing EmbeddedDefaults at the tree is a +// behavioral no-op. (The Layer-2 matrix is the decision-level confirmation.) +func TestEmbeddedTreeMatchesYAML(t *testing.T) { + tree, err := EmbeddedPolicyTree() + if err != nil { + t.Fatalf("embedded tree: %v", err) + } + assembled := tree.Assemble() + legacy, err := parseBytes(defaultsBytes) + if err != nil { + t.Fatalf("parse legacy yaml: %v", err) + } + if !reflect.DeepEqual(assembled, legacy) { + t.Errorf("assembled per-depth tree != legacy defaults.zddc.yaml\n assembled=%+v\n legacy=%+v", assembled, legacy) + } +} + func keysOf(t PolicyTree) []string { out := make([]string, 0, len(t)) for k := range t {