ZDDC/zddc/internal/zddc/zippolicy.go
ZDDC 21f6883157 feat(zddc): embed default tree + assemble into cascade (migration phases 3-4)
Phase 3 — //go:embed all:defaults bakes the per-depth default tree into the
binary; EmbeddedPolicyTree() loads it (LoadPolicyTreeFromFS, generalized to any
fs.FS — embed, disk, or zip).

Phase 4 — PolicyTree.Assemble() folds the flat per-depth tree into the single
nested paths:-bearing ZddcFile the cascade walker already consumes, so the
walker is UNCHANGED. EmbeddedDefaults() now sources from the tree via Assemble()
instead of parsing defaults.zddc.yaml.

Proven behavior-preserving: TestEmbeddedTreeMatchesYAML asserts Assemble(tree)
deep-equals the legacy parsed defaults.zddc.yaml, and the Layer-2 matrix +
full suite stay green. defaults.zddc.yaml is kept only as that test's oracle
(deleted in phase 6). This same Assemble path is what an operator .zddc.zip
mounted at any level will use next (phase 5).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 11:22:59 -05:00

170 lines
5.1 KiB
Go

package zddc
import (
"io/fs"
"os"
"path"
"strings"
)
// PolicyTree is a set of .zddc documents addressed by directory path relative
// to a mount point, with "*" as the any-segment wildcard. It is the in-memory
// form of a per-depth default tree or an operator .zddc.zip dropped at a
// directory: mounting the tree at directory D means key "working" governs
// D/working/, "*/mdl" governs D/<anyproject>/mdl/, and "" is D's own .zddc.
//
// Resolution mirrors the paths: cascade — a literal segment beats "*" — so a
// .zddc.zip and a paths: block compose identically. A .zddc.zip can therefore
// be dropped at ANY level to contribute a whole policy subtree; combined with
// inherit:false in its resolved .zddc it becomes a self-contained island.
type PolicyTree map[string]ZddcFile
// AnyPlaceholder is the on-disk directory name standing in for the "*" wildcard
// in the embedded default-tree source (internal/zddc/defaults/), so the repo
// holds no shell-/go:embed-hostile literal "*" directories. Operator .zddc.zip
// bundles use "*" directly.
const AnyPlaceholder = "_any_"
// segsOf splits a "/"-joined member-dir key into segments ("" → no segments).
func segsOf(key string) []string {
if key == "" {
return nil
}
return strings.Split(key, "/")
}
// resolveTreeDir returns the member-dir key governing segs: same length, each
// key segment literal-equal or "*", most-literal wins (a literal beats "*" at
// the earliest differing position — matching the paths: literal-first rule).
func (t PolicyTree) resolveTreeDir(segs []string) (string, bool) {
bestKey := ""
var bestSegs []string
found := false
for key := range t {
ks := segsOf(key)
if len(ks) != len(segs) {
continue
}
match := true
for i := range ks {
if ks[i] != "*" && ks[i] != segs[i] {
match = false
break
}
}
if !match {
continue
}
if !found || moreLiteral(ks, bestSegs) {
bestKey, bestSegs, found = key, ks, true
}
}
return bestKey, found
}
// moreLiteral reports whether a is more specific than b: at the earliest
// position where one is literal and the other "*", the literal wins.
func moreLiteral(a, b []string) bool {
for i := range a {
al, bl := a[i] != "*", b[i] != "*"
if al != bl {
return al
}
}
return false
}
// Along returns the .zddc documents this tree contributes along relSegs — one
// per cascade level from the mount root (the empty prefix) down to the full
// path, in root→leaf order (matching PolicyChain.Levels indexing). Levels with
// no governing member contribute nothing.
func (t PolicyTree) Along(relSegs []string) []ZddcFile {
var out []ZddcFile
for k := 0; k <= len(relSegs); k++ {
if key, ok := t.resolveTreeDir(relSegs[:k]); ok {
out = append(out, t[key])
}
}
return out
}
// 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 := 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 := strings.TrimPrefix(path.Dir(p), root)
rel = strings.Trim(rel, "/")
key := ""
if rel != "" {
parts := strings.Split(rel, "/")
for i, s := range parts {
if s == AnyPlaceholder {
parts[i] = "*"
}
}
key = strings.Join(parts, "/")
}
data, err := fs.ReadFile(fsys, p)
if err != nil {
return err
}
zf, err := parseBytes(data)
if err != nil {
return err
}
out[key] = zf
return nil
})
if err != nil {
return nil, err
}
return out, nil
}
// LoadPolicyTreeFromDir is the on-disk convenience over LoadPolicyTreeFromFS.
func LoadPolicyTreeFromDir(fsDir string) (PolicyTree, error) {
return LoadPolicyTreeFromFS(os.DirFS(fsDir), ".")
}