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>
This commit is contained in:
parent
7e3dbe81aa
commit
21f6883157
3 changed files with 108 additions and 24 deletions
|
|
@ -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_ROOT>/.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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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), ".")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue