feat(server): creator-owned working folders; document-controller-gated root files

Replace the project-level working/<email> "personal workspace" idea (too much
complexity for too little) with a simpler model on the virtual <project>/working/:

- EnsureCanonicalAncestors now materialises the working/ slot dir on disk the
  first time real content is created beneath it (it stays a plain dir, never
  auto-owned). ssr/mdl/rsk/staging/reviewing keep rejecting physical writes.
- Each <project>/working/<folder>/ a user creates gets an unfenced auto-own
  .zddc (creator rwcda; the team inherits read+create-new, not w/d). history:
  true still inherits in, so markdown drafts there are versioned.
- defaults grant project_team rc + document_controller rwc at working/ so users
  can create their folders and the DC has authority throughout.
- A bare file DIRECTLY at the working/ root is reserved for the
  document_controller: serveFilePut and serveFileMove reject non-DC writes/moves
  there (isProjectWorkingRootFile + zddc.IsRoleMemberAt), independent of the ACL
  verb since mkdir and file-PUT both authorise as ActionCreate. Users work inside
  a folder; the DC creates files at the root or promotes one up with a MOVE.

Tests: ensure_test materialisation + plain-slot cases; fileapi_test DC-gate for
PUT and MOVE. The generic dispatch-routing test moves its ops into working/drafts/.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZDDC 2026-06-01 10:05:26 -05:00
parent 5b8bcaed89
commit 0d21c16102
8 changed files with 269 additions and 30 deletions

View file

@ -684,7 +684,9 @@ There are **no hardcoded folder names** — the canonical project structure is d
**Project shape (after the May 2026 reshape).** `archive/` is the only physical project-root directory. Everything party-scoped lives uniformly under `archive/<party>/{ssr.yaml, mdl/, rsk/, received/, issued/, incoming/, working/<email>/, staging/<batch>/, reviewing/<tracking>/}`. Six sibling top-level URLs are **virtual aggregators**, never on disk:
- **Row rollups** (tables tool, `default_tool: tables`) — `<project>/ssr`, `<project>/mdl`, `<project>/rsk`. Synthesise one row per party (SSR) or per row file across parties (MDL/RSK), with the source party injected as a synthesised `$party` column. The `$` sigil marks the column system-managed: the tables tool renders it read-only and strips it before submitting a write. Form-mode "+ Add row" on a rollup view prompts for `party` (the routing key, stored in the form schema as a real input field; stripped on write because the folder name *is* the identity).
- **Folder-nav aggregators** (browse tool, `default_tool: browse`) — `<project>/working`, `<project>/staging`, `<project>/reviewing`. List the parties whose `archive/<party>/<slot>/` has non-empty content (the in-flight filter — empty or .zddc-only slots are suppressed). Per-party URLs `<project>/<slot>/<party>[/<rest>]` 302-redirect to the canonical `<project>/archive/<party>/<slot>[/<rest>]`. No writes through the virtual URL space; sharing/bookmarks land on the canonical path after the redirect.
- **Folder-nav aggregators** (browse tool, `default_tool: browse`) — `<project>/working`, `<project>/staging`, `<project>/reviewing`. List the parties whose `archive/<party>/<slot>/` has non-empty content (the in-flight filter — empty or .zddc-only slots are suppressed). Per-party URLs `<project>/<slot>/<party>[/<rest>]` 302-redirect to the canonical `<project>/archive/<party>/<slot>[/<rest>]`. (A party name fails `ValidPartyName` only if it contains a character outside `[A-Za-z0-9.-]` — the resolver then declines to redirect and the path is treated physically; see `working/` below.) Sharing/bookmarks land on the canonical path after the redirect.
`working/` is the one folder-nav aggregator that **also materialises on disk**: it doubles as a shared project-level drafting space holding **creator-owned working folders** at `<project>/working/<folder>/`. The slot dir is instantiated lazily by `EnsureCanonicalAncestors` the first time real content is created beneath it (it stays a plain dir — never auto-owned), and each `<folder>/` a user creates gets an *unfenced* auto-own `.zddc` (`history: true` inherits in, so markdown drafts there are versioned). Authorisation splits dir-vs-file at the root: project members may create folders (`project_team: rc` in the defaults), but a **bare file directly at the `working/` root is reserved for the `document_controller`** — regular users work inside a folder; the DC creates files at the root or promotes one up with a MOVE. Enforced in `serveFilePut`/`serveFileMove` via `isProjectWorkingRootFile` + `zddc.IsRoleMemberAt`, independent of the ACL verb (since mkdir and file-PUT both authorise as `ActionCreate`). The earlier per-user `working/<email>/` "personal workspace" idea was dropped as more complexity than it earned. `staging`/`reviewing` remain non-materialising — `EnsureCanonicalAncestors` still rejects physical writes under them.
Mkdir at the project root is restricted: only `archive` and `_`/`.`-prefixed system names are accepted (`handler/fileapi.go: rejectProjectRootMkdir`). Any other name — including the six virtual aggregator names, which would shadow the virtual surface — returns 409 Conflict. This is the only structural mkdir guard; deeper paths are governed by `auto_own:` + `worm:` + ACL.

View file

@ -298,9 +298,11 @@ func TestDispatchRoutesWritesToFileAPI(t *testing.T) {
return req.WithContext(handler.WithEmail(req.Context(), email))
}
// PUT a new file via dispatch.
// PUT a new file via dispatch. Files live in a sub-folder under
// working/ (creator-owned); bare files at the working/ root are
// document-controller-only (see TestFileAPI_WorkingRootFileDocControllerOnly).
body := []byte("note body")
req := withEmail(httptest.NewRequest(http.MethodPut, "/Project-A/Working/note.md", strings.NewReader(string(body))), "alice@example.com")
req := withEmail(httptest.NewRequest(http.MethodPut, "/Project-A/Working/drafts/note.md", strings.NewReader(string(body))), "alice@example.com")
rec := httptest.NewRecorder()
dispatch(cfg, idx, ring, nil, nil, rec, req)
if rec.Code != http.StatusCreated {
@ -308,7 +310,7 @@ func TestDispatchRoutesWritesToFileAPI(t *testing.T) {
}
// GET it back.
req = withEmail(httptest.NewRequest(http.MethodGet, "/Project-A/Working/note.md", nil), "alice@example.com")
req = withEmail(httptest.NewRequest(http.MethodGet, "/Project-A/Working/drafts/note.md", nil), "alice@example.com")
rec = httptest.NewRecorder()
dispatch(cfg, idx, ring, nil, nil, rec, req)
if rec.Code != http.StatusOK || rec.Body.String() != string(body) {
@ -316,9 +318,9 @@ func TestDispatchRoutesWritesToFileAPI(t *testing.T) {
}
// MOVE it.
req = withEmail(httptest.NewRequest(http.MethodPost, "/Project-A/Working/note.md", nil), "alice@example.com")
req = withEmail(httptest.NewRequest(http.MethodPost, "/Project-A/Working/drafts/note.md", nil), "alice@example.com")
req.Header.Set("X-ZDDC-Op", "move")
req.Header.Set("X-ZDDC-Destination", "/Project-A/Working/renamed.md")
req.Header.Set("X-ZDDC-Destination", "/Project-A/Working/drafts/renamed.md")
rec = httptest.NewRecorder()
dispatch(cfg, idx, ring, nil, nil, rec, req)
if rec.Code != http.StatusOK {
@ -326,7 +328,7 @@ func TestDispatchRoutesWritesToFileAPI(t *testing.T) {
}
// DELETE it.
req = withEmail(httptest.NewRequest(http.MethodDelete, "/Project-A/Working/renamed.md", nil), "alice@example.com")
req = withEmail(httptest.NewRequest(http.MethodDelete, "/Project-A/Working/drafts/renamed.md", nil), "alice@example.com")
rec = httptest.NewRecorder()
dispatch(cfg, idx, ring, nil, nil, rec, req)
if rec.Code != http.StatusNoContent {
@ -1063,4 +1065,3 @@ func TestGzhttpWrapper_CompressesLargeResponses(t *testing.T) {
}
})
}

View file

@ -392,6 +392,17 @@ func serveFilePut(cfg config.Config, w http.ResponseWriter, r *http.Request) {
if !authorizeAction(cfg, w, r, abs, cleanURL, action) {
return
}
// Files placed DIRECTLY at the project-level virtual working/ root are
// reserved for the document controller. Regular users create folders
// under working/ and work inside them; the DC creates files at the
// root or promotes them up from a folder (see serveFileMove). Files in
// sub-folders and the working/.zddc config are unaffected.
if filepath.Base(abs) != ".zddc" && isProjectWorkingRootFile(cfg.Root, abs) &&
!zddc.IsRoleMemberAt(cfg.Root, filepath.Dir(abs), "document_controller", EmailFromContext(r)) {
auditFile(r, "put", cleanURL, http.StatusForbidden, 0, nil)
http.Error(w, "Forbidden — files cannot be created directly in working/; create a folder and work inside it. Only the document controller may place files at the working/ root.", http.StatusForbidden)
return
}
if !checkIfMatch(w, r, abs) {
return
}
@ -655,6 +666,13 @@ func serveFileMove(cfg config.Config, w http.ResponseWriter, r *http.Request) {
if !authorizeAction(cfg, w, r, dstAbs, dstURL, policy.ActionCreate) {
return
}
// Promoting a file to the project-level working/ root is reserved for
// the document controller, same as a direct create there (serveFilePut).
if filepath.Base(dstAbs) != ".zddc" && isProjectWorkingRootFile(cfg.Root, dstAbs) &&
!zddc.IsRoleMemberAt(cfg.Root, filepath.Dir(dstAbs), "document_controller", EmailFromContext(r)) {
http.Error(w, "Forbidden — only the document controller may move files to the working/ root.", http.StatusForbidden)
return
}
if !checkIfMatch(w, r, srcAbs) {
return
}
@ -810,6 +828,26 @@ func serveFileMkdir(cfg config.Config, w http.ResponseWriter, r *http.Request) {
// Returns (true, reason) when the request should be 409'd. Returns
// (false, "") when the target is at any other depth or carries an
// allowed name.
// isProjectWorkingRootFile reports whether abs targets a file sitting
// directly in the project-level virtual working/ aggregator —
// <project>/working/<file> — as opposed to a file inside a sub-folder
// (<project>/working/<folder>/<file>, depth 4+) or anywhere else.
// Used to gate file creation/promotion at the working/ root to the
// document controller; everything deeper is ordinary creator-owned
// working space.
func isProjectWorkingRootFile(fsRoot, abs string) bool {
rel, err := filepath.Rel(fsRoot, abs)
if err != nil {
return false
}
rel = filepath.ToSlash(rel)
if strings.HasPrefix(rel, "../") || rel == "." {
return false
}
parts := strings.Split(rel, "/")
return len(parts) == 3 && strings.EqualFold(parts[1], "working")
}
func rejectProjectRootMkdir(fsRoot, abs string) (bool, string) {
rel, err := filepath.Rel(fsRoot, abs)
if err != nil {

View file

@ -720,3 +720,104 @@ func TestFileAPI_AutoMkdirNotInIssued(t *testing.T) {
// archive/<party>/staging/<batch>/ and working at archive/<party>/
// working/<email>/, the project-level pairing no longer maps cleanly.
// Tests for the removed behaviour have been deleted.)
// workingRootSetup builds a root that grants the team rwcd and names a
// document controller (the standard role ships empty; a deployment
// populates it). Returns the same do() helper shape as fileAPITestSetup.
func workingRootSetup(t *testing.T) (root string, do func(method, target, email string, body []byte, headers map[string]string) *httptest.ResponseRecorder) {
t.Helper()
root = t.TempDir()
rootZddc := "acl:\n permissions:\n \"*@example.com\": rwcd\n" +
"roles:\n document_controller:\n members: [dc@example.com]\n"
if err := os.WriteFile(filepath.Join(root, ".zddc"), []byte(rootZddc), 0o644); err != nil {
t.Fatalf("write root .zddc: %v", err)
}
zddc.InvalidateCache(root)
cfg := config.Config{Root: root, EmailHeader: "X-Auth-Request-Email", MaxWriteBytes: 1 << 20}
do = func(method, target, email string, body []byte, headers map[string]string) *httptest.ResponseRecorder {
var req *http.Request
if body != nil {
req = httptest.NewRequest(method, target, bytes.NewReader(body))
} else {
req = httptest.NewRequest(method, target, nil)
}
for k, v := range headers {
req.Header.Set(k, v)
}
ctx := context.WithValue(req.Context(), EmailKey, email)
ctx = context.WithValue(ctx, ElevatedKey, true)
req = req.WithContext(ctx)
rec := httptest.NewRecorder()
ServeFileAPI(cfg, rec, req)
return rec
}
return root, do
}
// Files directly at the project-level working/ root are document-
// controller-only; folders (and files inside them) are open to the team
// and become creator-owned.
func TestFileAPI_WorkingRootFileDocControllerOnly(t *testing.T) {
root, do := workingRootSetup(t)
// Non-DC user: a bare file at the working/ root is forbidden.
if rec := do(http.MethodPut, "/Proj/working/memo.md", "alice@example.com", []byte("x"), nil); rec.Code != http.StatusForbidden {
t.Errorf("non-DC file at working/ root: want 403, got %d: %s", rec.Code, rec.Body.String())
}
if _, err := os.Stat(filepath.Join(root, "Proj", "working", "memo.md")); !os.IsNotExist(err) {
t.Errorf("forbidden file must not be written")
}
// Same user CAN create a folder and work inside it; the folder
// becomes creator-owned (unfenced auto-own .zddc).
if rec := do(http.MethodPut, "/Proj/working/drafts/notes.md", "alice@example.com", []byte("x"), nil); rec.Code != http.StatusCreated {
t.Fatalf("user file inside working folder: want 201, got %d: %s", rec.Code, rec.Body.String())
}
z, err := os.ReadFile(filepath.Join(root, "Proj", "working", "drafts", ".zddc"))
if err != nil {
t.Fatalf("creator-owned folder auto-own .zddc missing: %v", err)
}
if !strings.Contains(string(z), "alice@example.com: rwcda") {
t.Errorf("creator folder should grant alice rwcda; got: %s", z)
}
// The document controller CAN place a file at the working/ root.
if rec := do(http.MethodPut, "/Proj/working/memo.md", "dc@example.com", []byte("x"), nil); rec.Code != http.StatusCreated {
t.Errorf("DC file at working/ root: want 201, got %d: %s", rec.Code, rec.Body.String())
}
}
// Promoting a file from a working folder up to the working/ root is
// document-controller-only, mirroring direct creation.
func TestFileAPI_WorkingRootMoveDocControllerOnly(t *testing.T) {
root, do := workingRootSetup(t)
// alice owns a folder with a file.
if rec := do(http.MethodPut, "/Proj/working/drafts/a.md", "alice@example.com", []byte("body"), nil); rec.Code != http.StatusCreated {
t.Fatalf("seed file: want 201, got %d: %s", rec.Code, rec.Body.String())
}
// alice cannot promote it to the working/ root.
rec := do(http.MethodPost, "/Proj/working/drafts/a.md", "alice@example.com", nil, map[string]string{
"X-ZDDC-Op": "move",
"X-ZDDC-Destination": "/Proj/working/a.md",
})
if rec.Code != http.StatusForbidden {
t.Errorf("non-DC move to working/ root: want 403, got %d: %s", rec.Code, rec.Body.String())
}
if _, err := os.Stat(filepath.Join(root, "Proj", "working", "a.md")); !os.IsNotExist(err) {
t.Errorf("forbidden move must not create the destination")
}
// The document controller can.
rec = do(http.MethodPost, "/Proj/working/drafts/a.md", "dc@example.com", nil, map[string]string{
"X-ZDDC-Op": "move",
"X-ZDDC-Destination": "/Proj/working/a.md",
})
if rec.Code != http.StatusOK {
t.Errorf("DC move to working/ root: want 200, got %d: %s", rec.Code, rec.Body.String())
}
if _, err := os.Stat(filepath.Join(root, "Proj", "working", "a.md")); err != nil {
t.Errorf("DC move should land the file at the root: %v", err)
}
}

View file

@ -208,19 +208,36 @@ paths:
available_tools: [browse]
# Project-level working is BOTH an outstanding-only aggregator of
# the per-party archive/<party>/working/ slots AND the home for
# each user's personal workspace at <project>/working/<email>/.
# shared, creator-owned working folders at <project>/working/
# <folder>/. (The earlier per-user <email>/ "personal workspace"
# concept was dropped — too much complexity for too little gain.
# A working folder is just a named drafting space its creator
# owns; nothing keys off the email.)
virtual: true
# Edit-history versioning for personal markdown drafts (inherits
# to the <email>/ homes below). archive/<party>/working/ carries
# its own history: true separately.
# Edit-history versioning for markdown drafts (inherits to the
# folders below). archive/<party>/working/ carries its own
# history: true separately.
history: true
# Project members may CREATE here (so they can make their own
# working folders) but the file handler additionally restricts
# bare files at this root to the document controller — users put
# files inside a folder they created; the DC promotes/creates
# files at the root. The DC carries rwc so it can do both (and,
# unfenced, that authority cascades into every folder below).
acl:
permissions:
project_team: rc
document_controller: rwc
paths:
"*":
# Per-user personal workspace: <project>/working/<email>/.
# A creator-owned working folder: <project>/working/<folder>/.
# auto_own (NOT auto_own_fenced) makes the creator the owner
# (rwcda + a .zddc they can edit) while leaving the home PUBLIC
# by default — the owner opts into privacy by adding
# inherit:false / restricting permissions in their own .zddc.
# (rwcda + a .zddc they can edit) while leaving the folder
# readable to the rest of the project_team via the unfenced
# cascade above. Other members inherit rc here (read + add
# new files) but lack w/d, so they cannot modify or delete
# the owner's files; the owner narrows this in their own
# .zddc if they want it private.
default_tool: browse
available_tools: [browse]
auto_own: true

View file

@ -84,13 +84,19 @@ func ResolveCanonicalPath(fsRoot, target string) (string, error) {
//
// Canonical positions, relative to fsRoot:
//
// - <project>/archive (the only physical project-root canonical;
// working/staging/reviewing/ssr/mdl/rsk at project root are virtual
// aggregators with no on-disk presence — writes targeting them
// must be rejected by the caller's project-root mkdir guard.)
// - <project>/archive (a physical project-root canonical)
//
// - <project>/working (virtual at the slot level, but materialised
// on disk to host creator-owned working folders + document-
// controller files; created as a plain dir, never auto-owned)
//
// staging/reviewing/ssr/mdl/rsk at project root are virtual
// aggregators with no on-disk presence — writes targeting them are
// rejected here and by the caller's project-root mkdir guard.
//
// - <project>/archive/<party>/<canonical-party> where
// <canonical-party> ∈ {mdl, rsk, incoming, received, issued,
// working, staging, reviewing}
// working, staging, reviewing}
//
// fsRoot and target must be absolute filesystem paths under the same
// volume; target may not yet exist on disk.
@ -110,13 +116,24 @@ func EnsureCanonicalAncestors(fsRoot, target, principalEmail string, perm fs.Fil
return target, nil
}
// Reject writes targeting top-level virtual aggregators —
// <project>/{ssr,mdl,rsk,working,staging,reviewing}/... — these
// resolve through ResolveVirtualView, not as physical paths. A
// caller writing under them bypassed the virtual resolver.
// Project-root virtual aggregators. ssr/mdl/rsk are pure tables
// rollups and staging/reviewing are synthesised lifecycle windows —
// none materialise on disk; row PUTs / lifecycle writes are rewritten
// to canonical archive/<party>/ paths by ResolveVirtualView before
// reaching here, so a physical write under one of these means the
// resolver was bypassed.
//
// working/ is the exception: it is virtual at the slot level (the
// listing is synthesised from archive/*/working/), but it is also
// the home for creator-owned working folders at <project>/working/
// <folder>/. The slot dir is instantiated on disk the moment real
// content is created beneath it. Fall through to materialise it +
// the target's ancestors; WHO may write here (users create folders;
// only the document controller places files at the root) is gated in
// the file handler, not here.
if len(parts) >= 2 {
switch strings.ToLower(parts[1]) {
case "ssr", "mdl", "rsk", "working", "staging", "reviewing":
case "ssr", "mdl", "rsk", "staging", "reviewing":
return target, fmt.Errorf("%s/ at project root is a virtual aggregator and not writable as a physical path", parts[1])
}
}

View file

@ -197,12 +197,12 @@ func TestEnsureCanonicalAncestors_NoPrincipalSkipsAutoOwn(t *testing.T) {
}
}
// Project-root virtual aggregator names are rejected — a write
// targeting <project>/working/<...> bypasses the virtual resolver
// and must not materialise on disk.
// Project-root virtual aggregator names (except working/) are rejected —
// a write targeting <project>/{staging,reviewing,ssr,mdl,rsk}/<...>
// bypasses the virtual resolver and must not materialise on disk.
func TestEnsureCanonicalAncestors_RejectsProjectRootVirtual(t *testing.T) {
root := t.TempDir()
for _, slot := range []string{"working", "staging", "reviewing", "ssr", "mdl", "rsk"} {
for _, slot := range []string{"staging", "reviewing", "ssr", "mdl", "rsk"} {
target := filepath.Join(root, "Proj", slot, "x.md")
_, err := EnsureCanonicalAncestors(root, target, "alice@x.com", 0o755)
if err == nil {
@ -214,6 +214,52 @@ func TestEnsureCanonicalAncestors_RejectsProjectRootVirtual(t *testing.T) {
}
}
// working/ is the exception: it hosts creator-owned working folders, so
// real content beneath it instantiates the (otherwise virtual) slot dir
// on disk. The slot dir itself stays plain (never auto-owned); the
// creator-owned folder under it gets the unfenced auto-own .zddc.
func TestEnsureCanonicalAncestors_WorkingMaterialisesCreatorFolder(t *testing.T) {
root := t.TempDir()
target := filepath.Join(root, "Proj", "working", "drafts", "notes.md")
if _, err := EnsureCanonicalAncestors(root, target, "alice@x.com", 0o755); err != nil {
t.Fatalf("ensure: %v", err)
}
if _, err := os.Stat(filepath.Join(root, "Proj", "working")); err != nil {
t.Errorf("working/ not created: %v", err)
}
if _, err := os.Stat(filepath.Join(root, "Proj", "working", ".zddc")); !os.IsNotExist(err) {
t.Errorf("working/ slot dir must stay plain (no auto-own .zddc); got err=%v", err)
}
folderZddc := filepath.Join(root, "Proj", "working", "drafts", ".zddc")
data, err := os.ReadFile(folderZddc)
if err != nil {
t.Fatalf("creator folder auto-own .zddc missing: %v", err)
}
if !strings.Contains(string(data), "alice@x.com: rwcda") {
t.Errorf("creator folder auto-own missing creator grant: %s", data)
}
if strings.Contains(string(data), "inherit: false") {
t.Errorf("creator working folder must be UNFENCED (readable by the team); got: %s", data)
}
}
// A bare file directly at the project-level working/ root still
// materialises the slot dir — the file handler gates WHO may write it,
// not EnsureCanonicalAncestors.
func TestEnsureCanonicalAncestors_WorkingRootFileMaterialisesSlot(t *testing.T) {
root := t.TempDir()
target := filepath.Join(root, "Proj", "working", "memo.md")
if _, err := EnsureCanonicalAncestors(root, target, "dc@x.com", 0o755); err != nil {
t.Fatalf("ensure: %v", err)
}
if _, err := os.Stat(filepath.Join(root, "Proj", "working")); err != nil {
t.Errorf("working/ not created: %v", err)
}
if _, err := os.Stat(filepath.Join(root, "Proj", "working", ".zddc")); !os.IsNotExist(err) {
t.Errorf("working/ slot dir must stay plain (no auto-own .zddc); got err=%v", err)
}
}
func TestEnsureCanonicalAncestors_RejectsTraversal(t *testing.T) {
root := t.TempDir()
other := t.TempDir()

View file

@ -159,6 +159,23 @@ func HistoryAt(fsRoot, dirPath string) bool {
return chain.EffectiveHistory()
}
// IsRoleMemberAt reports whether email is a member of roleName as the
// role is visible (after fences / resets, unioned with the embedded
// defaults) at dirPath's cascade leaf. Returns false for an empty
// email or on cascade error. Used by the file handler to gate the few
// operations reserved for a standard role — e.g. only the
// document_controller may place files directly at the working/ root.
func IsRoleMemberAt(fsRoot, dirPath, roleName, email string) bool {
if email == "" {
return false
}
chain, err := EffectivePolicy(fsRoot, dirPath)
if err != nil || len(chain.Levels) == 0 {
return false
}
return MatchesPrincipal(roleName, email, chain, len(chain.Levels)-1)
}
// IsDeclaredPath reports whether dirPath is mentioned in the
// cascade — either by an on-disk .zddc at that level OR by any
// ancestor's paths: tree (including the embedded defaults).