feat(zddc): policy-tree resolver + per-depth default tree (migration phases 1-2)

Foundation for replacing the single embedded defaults.zddc.yaml with a
.zddc.zip policy SUBTREE mountable at any directory. defaults.zddc.yaml stays
live and authoritative for now — this is purely additive.

Phase 1 — author the per-depth default tree under internal/zddc/defaults/, one
focused .zddc per canonical folder (root, */, */archive, */working[/*], */ssr,
*/mdl[/*], */rsk[/*], */staging[/*], */reviewing[/*], */incoming[/*]). The
`_any_` directory is the on-disk stand-in for the "*" wildcard, so the repo
holds no shell-/go:embed-hostile literal "*" directories.

Phase 2 — PolicyTree (internal/zddc/zippolicy.go): a set of .zddc documents
keyed by member dir relative to a mount point, with "*" wildcards.
resolveTreeDir does literal-first, most-specific segment matching (mirrors the
paths: cascade); Along returns the governing member at each cascade level
root→leaf; LoadPolicyTreeFromDir loads the source tree (mapping _any_ → *).
This is the engine for "drop a .zddc.zip at any level"; inherit:false in a
resolved member makes that subtree a self-contained island (existing fence
mechanism, unchanged).

Tests: resolver matching mechanics; the split tree loads with the expected keys
+ content (data-level faithfulness — full effective-policy parity is the
Layer-2 matrix once the cascade is wired in Phase 4); Along ordering.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZDDC 2026-06-05 10:40:34 -05:00
parent a84bdfdc58
commit 7e3dbe81aa
18 changed files with 370 additions and 0 deletions

View file

@ -0,0 +1,15 @@
# Embedded default policy — site root (mount point of the default tree).
# The bottom of every cascade unless an operator .zddc / .zddc.zip overrides.
# Authored per-depth; the `_any_` directory maps to the `*` (any-segment)
# wildcard when packaged into defaults.zddc.zip.
title: "ZDDC"
acl:
permissions: {}
roles:
document_controller:
members: []
project_team:
members: []
observer:
members: []
available_tools: [archive, browse, landing]

View file

@ -0,0 +1,7 @@
# Project level (any project name): read across the project; create only at the
# specific peers below — none gets `c` here.
acl:
permissions:
project_team: r
observer: r
document_controller: rw

View file

@ -0,0 +1,7 @@
# The committed record: pure WORM. Cascades to <party>/{received,issued}.
default_tool: archive
party_source: ssr
worm: [document_controller]
acl:
permissions:
document_controller: rwc

View file

@ -0,0 +1,6 @@
default_tool: classifier
available_tools: [classifier]
party_source: ssr
acl:
permissions:
document_controller: rwcd

View file

@ -0,0 +1,2 @@
auto_own: true
drop_target: true

View file

@ -0,0 +1,8 @@
default_tool: tables
available_tools: [tables]
party_source: ssr
history: true
acl:
permissions:
document_controller: rwcd
project_team: rwc

View file

@ -0,0 +1,6 @@
default_tool: tables
records:
"*.yaml":
folder_fields:
originator: 0
filename_format: "{originator}-{project}-{discipline}-{type}-{sequence}-{suffix?}"

View file

@ -0,0 +1,7 @@
default_tool: browse
available_tools: [browse]
party_source: ssr
acl:
permissions:
project_team: cr
document_controller: rwcda

View file

@ -0,0 +1,2 @@
auto_own: true
drop_target: true

View file

@ -0,0 +1,8 @@
default_tool: tables
available_tools: [tables]
party_source: ssr
history: true
acl:
permissions:
document_controller: rwcd
project_team: rwc

View file

@ -0,0 +1,11 @@
default_tool: tables
records:
"*.yaml":
folder_fields:
originator: 0
filename_format: "{originator}-{project}-{discipline}-{type}-{sequence}-{suffix?}-{row}"
field_defaults:
type: RSK
locked: [type]
row_field: row
row_scope_fields: [originator, project, discipline, type, sequence, suffix]

View file

@ -0,0 +1,12 @@
# Authoritative party registry + submittal status register. NO party_source.
default_tool: tables
available_tools: [tables]
acl:
permissions:
document_controller: rwc
history: true
records:
"*.yaml":
field_defaults:
kind: SSR
locked: [kind]

View file

@ -0,0 +1,7 @@
default_tool: transmittal
available_tools: [transmittal, classifier]
party_source: ssr
acl:
permissions:
project_team: cr
document_controller: rwcda

View file

@ -0,0 +1,2 @@
auto_own: true
drop_target: true

View file

@ -0,0 +1,8 @@
default_tool: browse
available_tools: [browse, classifier]
party_source: ssr
history: true
acl:
permissions:
project_team: cr
document_controller: rwcda

View file

@ -0,0 +1,2 @@
auto_own: true
drop_target: true

View file

@ -0,0 +1,132 @@
package zddc
import (
"io/fs"
"os"
"path/filepath"
"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
}
// 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) {
out := PolicyTree{}
err := filepath.WalkDir(fsDir, 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
}
key := ""
if rel != "." {
parts := strings.Split(filepath.ToSlash(rel), "/")
for i, s := range parts {
if s == AnyPlaceholder {
parts[i] = "*"
}
}
key = strings.Join(parts, "/")
}
data, err := os.ReadFile(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
}

View file

@ -0,0 +1,128 @@
package zddc
import (
"reflect"
"testing"
)
// resolver matching mechanics: literal beats "*", length must match, most-
// specific wins, empty path → root.
func TestPolicyTreeResolve(t *testing.T) {
tree := PolicyTree{
"": {Title: "root"},
"*": {Title: "project"},
"working": {Title: "literal-working"},
"*/working": {Title: "any-working"},
"*/working/*": {Title: "any-working-any"},
"*/mdl": {Title: "any-mdl"},
"Proj/working": {Title: "proj-working"},
}
cases := []struct {
segs []string
want string // resolved key
ok bool
}{
{nil, "", true}, // root
{[]string{"Proj"}, "*", true}, // no literal "Proj" at level 1 → "*"
{[]string{"working"}, "working", true}, // literal beats "*"
{[]string{"Proj", "working"}, "Proj/working", true}, // both literal beats "*/working"
{[]string{"Other", "working"}, "*/working", true}, // first seg "*", second literal
{[]string{"Proj", "mdl"}, "*/mdl", true},
{[]string{"Proj", "working", "Acme"}, "*/working/*", true},
{[]string{"Proj", "nope"}, "", false}, // no len-2 key matches "*/nope"
}
for _, c := range cases {
got, ok := tree.resolveTreeDir(c.segs)
if ok != c.ok || (ok && got != c.want) {
t.Errorf("resolveTreeDir(%v) = (%q,%v), want (%q,%v)", c.segs, got, ok, c.want, c.ok)
}
}
}
// Load the real embedded-default source tree and assert the split content
// reproduces the intended document-control policy (the faithfulness check at
// the data level; full effective-policy parity is the Layer-2 matrix once the
// cascade is wired in Phase 4).
func TestLoadPolicyTreeFromDir(t *testing.T) {
tree, err := LoadPolicyTreeFromDir("defaults")
if err != nil {
t.Fatalf("load defaults tree: %v", err)
}
wantKeys := []string{
"", "*", "*/archive", "*/ssr", "*/incoming", "*/incoming/*",
"*/reviewing", "*/reviewing/*", "*/working", "*/working/*",
"*/staging", "*/staging/*", "*/mdl", "*/mdl/*", "*/rsk", "*/rsk/*",
}
for _, k := range wantKeys {
if _, ok := tree[k]; !ok {
t.Errorf("missing tree key %q", k)
}
}
if len(tree) != len(wantKeys) {
t.Errorf("tree has %d keys, want %d: %v", len(tree), len(wantKeys), keysOf(tree))
}
// Spot-check the policy each member carries.
if got := tree[""].AvailableTools; !reflect.DeepEqual(got, []string{"archive", "browse", "landing"}) {
t.Errorf("root available_tools = %v", got)
}
if _, ok := tree[""].Roles["document_controller"]; !ok {
t.Errorf("root missing document_controller role")
}
if got := tree["*"].ACL.Permissions["project_team"]; got != "r" {
t.Errorf("project-level project_team = %q, want r", got)
}
if got := tree["*"].ACL.Permissions["document_controller"]; got != "rw" {
t.Errorf("project-level document_controller = %q, want rw", got)
}
if got := tree["*/working"].ACL.Permissions["document_controller"]; got != "rwcda" {
t.Errorf("working document_controller = %q, want rwcda", got)
}
if got := tree["*/working"].ACL.Permissions["project_team"]; got != "cr" {
t.Errorf("working project_team = %q, want cr", got)
}
if tree["*/working"].PartySource != "ssr" {
t.Errorf("working party_source = %q, want ssr", tree["*/working"].PartySource)
}
if h := tree["*/working"].History; h == nil || !*h {
t.Errorf("working history not true")
}
if got := tree["*/mdl"].ACL.Permissions["project_team"]; got != "rwc" {
t.Errorf("mdl project_team = %q, want rwc", got)
}
if got := tree["*/archive"].Worm; !reflect.DeepEqual(got, []string{"document_controller"}) {
t.Errorf("archive worm = %v, want [document_controller]", got)
}
if ao := tree["*/working/*"].AutoOwn; ao == nil || !*ao {
t.Errorf("working/<party> auto_own not true")
}
}
// Along returns the governing members in root→leaf order for a path.
func TestPolicyTreeAlong(t *testing.T) {
tree, err := LoadPolicyTreeFromDir("defaults")
if err != nil {
t.Fatalf("load: %v", err)
}
levels := tree.Along([]string{"Proj", "working", "Acme"})
// "", "*", "*/working", "*/working/*" → 4 contributing levels.
if len(levels) != 4 {
t.Fatalf("Along returned %d levels, want 4", len(levels))
}
// Leaf level is working/<party>: auto_own.
if ao := levels[3].AutoOwn; ao == nil || !*ao {
t.Errorf("leaf level should carry auto_own")
}
// The working level grants the DC full authority.
if got := levels[2].ACL.Permissions["document_controller"]; got != "rwcda" {
t.Errorf("level 2 (working) document_controller = %q, want rwcda", got)
}
}
func keysOf(t PolicyTree) []string {
out := make([]string, 0, len(t))
for k := range t {
out = append(out, k)
}
return out
}