feat(zddc): Phase 3a — populated defaults + cascade lookup helpers
Schema: - default_tool: string (tool name served at this dir's no-slash URL) - auto_own: *bool (mkdir post-hook auto-grants the creator) - virtual: *bool (never materialise on disk; aggregator routes) defaults.zddc.yaml: populated with the full canonical convention via paths:. Top-level "*" matches any project; nested archive/working/ staging/reviewing declare the project-stage tools; archive's "*" / mdl|incoming|received|issued tree declares the per-party surfaces. All four party folders and all four project-root folders get their default_tool; working / staging / archive/<party>/incoming get auto_own; reviewing / archive/<party>/mdl get virtual. None of these need on-disk dirs to exist. Lookups (zddc/internal/zddc/lookups.go): DefaultToolAt(root, dir) → cascade-resolved default tool name AutoOwnAt(root, dir) → does mkdir auto-own here? VirtualAt(root, dir) → never materialise on disk? IsDeclaredPath(root, dir) → does the cascade say anything about this dir? ChildrenDeclaredAt(root, dir)→ literal child names declared by Paths Each looks up via EffectivePolicy → leaf level → Embedded fallback, so operators' on-disk overrides win and the embedded baseline carries the convention. Tests cover the embedded convention, operator overrides, and inherit:false blocking the embedded layer. No consumer migration yet — that's Phase 3b. Behaviour is bit-identical for current callers since none of them consult the new lookups. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2f08418fb0
commit
ea0d29ed17
6 changed files with 450 additions and 14 deletions
|
|
@ -1300,7 +1300,7 @@ body.help-open .app-header {
|
|||
</svg>
|
||||
<div class="header-title-group">
|
||||
<span class="app-header__title" id="table-title">ZDDC Table</span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-alpha · 2026-05-11 19:54:48 · d84c190-dirty</span></span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-alpha · 2026-05-11 20:00:21 · 2f08418-dirty</span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
|
|
|
|||
|
|
@ -4,26 +4,80 @@
|
|||
# at the on-disk root /.zddc (or any deeper level); to ignore this
|
||||
# file entirely, set `inherit: false` on an on-disk .zddc.
|
||||
#
|
||||
# Phase 1 of the .zddc-first-config rollout. Future phases will move
|
||||
# the hardcoded canonical-folder behaviour (ProjectRootFolders,
|
||||
# PartyFolders, apps.DefaultAppAt, etc.) into this file via a new
|
||||
# `paths:` recursive map + a few new per-directory keys (default_tool,
|
||||
# auto_own, virtual). For now this file is intentionally minimal —
|
||||
# the plumbing exists, the schema doesn't.
|
||||
#
|
||||
# Read-only at runtime; the binary does not write to its embedded
|
||||
# copy. To export an editable copy for an operator:
|
||||
# To export an editable copy for an operator:
|
||||
#
|
||||
# zddc-server show-defaults > /var/lib/zddc/root/.zddc
|
||||
#
|
||||
# That places this file at the on-disk root, where the operator can
|
||||
# edit it freely. The new file then takes the place of the embedded
|
||||
# one (no double-counting — both contribute to the cascade, leaf wins).
|
||||
# one (both contribute to the cascade, on-disk wins per-field).
|
||||
|
||||
title: "ZDDC"
|
||||
|
||||
# Phase 1: empty acl + empty admins, equivalent to "the embedded
|
||||
# layer grants nothing; rules come from on-disk .zddc files above".
|
||||
# This preserves bit-identical behaviour for existing deployments.
|
||||
# Empty acl at this layer — rules come from on-disk .zddc files above.
|
||||
# A deployment with no on-disk root .zddc grants no access (consistent
|
||||
# with prior behaviour); operators bootstrap by editing the root file.
|
||||
acl:
|
||||
permissions: {}
|
||||
|
||||
# ── Canonical project structure ────────────────────────────────────────────
|
||||
#
|
||||
# Every ZDDC project lives at a top-level directory. Under it the
|
||||
# convention is four canonical folders: archive (formal record),
|
||||
# working (in-progress workspace), staging (outbound prep), reviewing
|
||||
# (purely virtual aggregator). Under archive/<party>/ the convention
|
||||
# is four more: mdl (deliverables list), incoming (counterparty drop
|
||||
# zone), received (immutable submittals), issued (immutable responses).
|
||||
#
|
||||
# All of this is expressed via the recursive paths: schema. None of
|
||||
# the directories need to exist on disk — the cascade walker resolves
|
||||
# behaviour from this declaration, so a fresh project lands on
|
||||
# usable empty views at every well-known URL.
|
||||
#
|
||||
# Operators override any of this by mirroring the structure in an
|
||||
# on-disk .zddc and changing what they need; on-disk values win.
|
||||
|
||||
paths:
|
||||
# First segment under root is the project name; "*" matches any.
|
||||
"*":
|
||||
paths:
|
||||
archive:
|
||||
default_tool: archive
|
||||
paths:
|
||||
# Second segment under archive/ is the party name.
|
||||
"*":
|
||||
paths:
|
||||
mdl:
|
||||
default_tool: tables
|
||||
# The mdl folder is virtual by convention — the
|
||||
# tables tool serves it from the embedded default
|
||||
# spec even when the on-disk folder doesn't exist.
|
||||
virtual: true
|
||||
incoming:
|
||||
default_tool: classifier
|
||||
# First write into incoming/ auto-creates an owner
|
||||
# grant so the creator can manage their own drops.
|
||||
auto_own: true
|
||||
received:
|
||||
default_tool: archive
|
||||
# received/ is WORM — express as ACL elsewhere; the
|
||||
# default convention is simply "no auto_own here".
|
||||
issued:
|
||||
default_tool: archive
|
||||
working:
|
||||
default_tool: mdedit
|
||||
# working/ auto-owns the first creator + the per-user homes
|
||||
# below.
|
||||
auto_own: true
|
||||
paths:
|
||||
"*": # per-user home dir
|
||||
default_tool: mdedit
|
||||
auto_own: true
|
||||
staging:
|
||||
default_tool: transmittal
|
||||
auto_own: true
|
||||
reviewing:
|
||||
default_tool: mdedit
|
||||
# reviewing/ is purely virtual — the aggregator handler
|
||||
# synthesises listings from received/ ↔ staging/ ↔ issued/.
|
||||
virtual: true
|
||||
|
|
|
|||
|
|
@ -171,6 +171,28 @@ type ZddcFile struct {
|
|||
// false. nil == defaults to true.
|
||||
Inherit *bool `yaml:"inherit,omitempty" json:"inherit,omitempty"`
|
||||
|
||||
// DefaultTool is the tool name served at this directory's
|
||||
// no-slash URL form (e.g. /Project/working without trailing slash
|
||||
// → mdedit). Empty means "no default" — the slash convention's
|
||||
// browse listing wins and the no-slash form 302s. Cascades
|
||||
// through Paths: an ancestor's Paths entry can set DefaultTool
|
||||
// for a virtual descendant without anyone creating that dir.
|
||||
DefaultTool string `yaml:"default_tool,omitempty" json:"default_tool,omitempty"`
|
||||
|
||||
// AutoOwn controls whether the file API's mkdir post-hook writes
|
||||
// an auto-owned .zddc granting the creator rwcda at the new
|
||||
// directory. Useful for working/staging/incoming-style drafting
|
||||
// surfaces where the first creator should "own" what they
|
||||
// created. Empty (nil) inherits via cascade.
|
||||
AutoOwn *bool `yaml:"auto_own,omitempty" json:"auto_own,omitempty"`
|
||||
|
||||
// Virtual marks a directory as never-materialise-on-disk. The
|
||||
// server treats requests under such a path as virtual routes
|
||||
// rather than triggering EnsureCanonicalAncestors. The reviewing
|
||||
// aggregator is the canonical example. Empty (nil) inherits via
|
||||
// cascade.
|
||||
Virtual *bool `yaml:"virtual,omitempty" json:"virtual,omitempty"`
|
||||
|
||||
// Paths declares virtual sub-directory rules without those
|
||||
// directories needing to exist on disk. Each key is a single path
|
||||
// segment — either a literal name or `*` (matches any segment).
|
||||
|
|
|
|||
179
zddc/internal/zddc/lookups.go
Normal file
179
zddc/internal/zddc/lookups.go
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
package zddc
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// DefaultToolAt returns the cascade-resolved default tool name for
|
||||
// the directory at dirPath. Empty when no cascade level (on-disk or
|
||||
// virtual via Paths) has declared a DefaultTool.
|
||||
//
|
||||
// Used by the URL dispatcher to route no-slash directory URLs (e.g.
|
||||
// /Project/working) to the appropriate tool. Replaces the hardcoded
|
||||
// apps.DefaultAppAt matrix once consumers are migrated (Phase 3b).
|
||||
func DefaultToolAt(fsRoot, dirPath string) string {
|
||||
chain, err := EffectivePolicy(fsRoot, dirPath)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
if dt := leafLevel(chain).DefaultTool; dt != "" {
|
||||
return dt
|
||||
}
|
||||
return chain.Embedded.DefaultTool
|
||||
}
|
||||
|
||||
// AutoOwnAt reports whether mkdir at this directory should write an
|
||||
// auto-owned .zddc. False (with explicit set) and unset both return
|
||||
// false to the caller; the file API only auto-owns when at least one
|
||||
// cascade level explicitly set auto_own: true.
|
||||
//
|
||||
// Replaces the AutoOwnCanonicalNames hardcoded list once the file
|
||||
// API's mkdir hook is migrated.
|
||||
func AutoOwnAt(fsRoot, dirPath string) bool {
|
||||
chain, err := EffectivePolicy(fsRoot, dirPath)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if v := leafLevel(chain).AutoOwn; v != nil {
|
||||
return *v
|
||||
}
|
||||
if v := chain.Embedded.AutoOwn; v != nil {
|
||||
return *v
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// VirtualAt reports whether the directory at dirPath is declared as
|
||||
// purely virtual (never materialise on disk). Used by
|
||||
// EnsureCanonicalAncestors to skip MkdirAll for these paths.
|
||||
//
|
||||
// Replaces the VirtualOnlyCanonicalNames hardcoded list once
|
||||
// consumers are migrated.
|
||||
func VirtualAt(fsRoot, dirPath string) bool {
|
||||
chain, err := EffectivePolicy(fsRoot, dirPath)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if v := leafLevel(chain).Virtual; v != nil {
|
||||
return *v
|
||||
}
|
||||
if v := chain.Embedded.Virtual; v != nil {
|
||||
return *v
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 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).
|
||||
//
|
||||
// A declared path is one the cascade has *something to say about*
|
||||
// even if the directory doesn't exist on disk. Used by listing
|
||||
// fallbacks to decide whether a missing directory should return an
|
||||
// empty listing (treat as virtual) vs 404 (truly unknown).
|
||||
//
|
||||
// Replaces IsProjectRootFolder + IsArchivePartyFolder once
|
||||
// consumers are migrated.
|
||||
func IsDeclaredPath(fsRoot, dirPath string) bool {
|
||||
chain, err := EffectivePolicy(fsRoot, dirPath)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if len(chain.Levels) == 0 {
|
||||
return false
|
||||
}
|
||||
leaf := leafLevel(chain)
|
||||
// A non-empty merged level at the leaf means at least one
|
||||
// contribution reached here — either an on-disk file, or an
|
||||
// ancestor's paths: glob matched.
|
||||
return !isZeroZddcFile(leaf)
|
||||
}
|
||||
|
||||
// ChildrenDeclaredAt returns the set of child directory names that
|
||||
// the cascade declares should exist under dirPath. Includes
|
||||
// wildcard "*" specs (caller decides how to expose those) and
|
||||
// literal names. Used by fs.ListDirectory to inject virtual
|
||||
// canonical-folder entries at a project root.
|
||||
//
|
||||
// Returns the literal names; "*" wildcards are NOT included
|
||||
// (callers can't synthesise a meaningful name for a wildcard).
|
||||
func ChildrenDeclaredAt(fsRoot, dirPath string) []string {
|
||||
chain, err := EffectivePolicy(fsRoot, dirPath)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
leaf := leafLevel(chain)
|
||||
if len(leaf.Paths) == 0 {
|
||||
return nil
|
||||
}
|
||||
var out []string
|
||||
for k := range leaf.Paths {
|
||||
if k == "*" {
|
||||
continue
|
||||
}
|
||||
out = append(out, k)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// leafLevel returns the deepest (most-specific) ZddcFile in chain.
|
||||
// Caller's responsibility to check len(chain.Levels) > 0 — but
|
||||
// returns ZddcFile{} on empty for ergonomic chaining.
|
||||
func leafLevel(chain PolicyChain) ZddcFile {
|
||||
if len(chain.Levels) == 0 {
|
||||
return ZddcFile{}
|
||||
}
|
||||
return chain.Levels[len(chain.Levels)-1]
|
||||
}
|
||||
|
||||
// isZeroZddcFile reports whether zf carries no meaningful content.
|
||||
// Used by IsDeclaredPath to distinguish "ancestor paths: matched
|
||||
// and stamped something here" from "no contribution at all".
|
||||
func isZeroZddcFile(zf ZddcFile) bool {
|
||||
if zf.Title != "" {
|
||||
return false
|
||||
}
|
||||
if zf.DefaultTool != "" {
|
||||
return false
|
||||
}
|
||||
if zf.AutoOwn != nil || zf.Virtual != nil || zf.Inherit != nil {
|
||||
return false
|
||||
}
|
||||
if zf.AppsPubKey != "" || zf.CreatedBy != "" {
|
||||
return false
|
||||
}
|
||||
if len(zf.Admins) > 0 {
|
||||
return false
|
||||
}
|
||||
if len(zf.ACL.Permissions) > 0 || len(zf.ACL.Allow) > 0 || len(zf.ACL.Deny) > 0 {
|
||||
return false
|
||||
}
|
||||
if zf.ACL.Inherit != nil {
|
||||
return false
|
||||
}
|
||||
if len(zf.Apps) > 0 || len(zf.Tables) > 0 || len(zf.Display) > 0 || len(zf.Paths) > 0 {
|
||||
return false
|
||||
}
|
||||
if len(zf.Roles) > 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// resolvePathSegments turns dirPath (absolute, under fsRoot) into a
|
||||
// slice of segments relative to fsRoot. Used by helpers that walk
|
||||
// the cascade by segment. Returns nil for dirPath == fsRoot or for
|
||||
// any path outside fsRoot.
|
||||
func resolvePathSegments(fsRoot, dirPath string) []string {
|
||||
fsRoot = filepath.Clean(fsRoot)
|
||||
dirPath = filepath.Clean(dirPath)
|
||||
if dirPath == fsRoot {
|
||||
return nil
|
||||
}
|
||||
rel, err := filepath.Rel(fsRoot, dirPath)
|
||||
if err != nil || strings.HasPrefix(rel, "..") {
|
||||
return nil
|
||||
}
|
||||
return strings.Split(rel, string(filepath.Separator))
|
||||
}
|
||||
172
zddc/internal/zddc/lookups_test.go
Normal file
172
zddc/internal/zddc/lookups_test.go
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
package zddc
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestDefaultToolAt_FromEmbeddedConvention — the canonical default-
|
||||
// tool rules in defaults.zddc.yaml should resolve correctly for the
|
||||
// well-known paths without any on-disk .zddc.
|
||||
func TestDefaultToolAt_FromEmbeddedConvention(t *testing.T) {
|
||||
resetCache()
|
||||
root := t.TempDir()
|
||||
cases := []struct {
|
||||
path string
|
||||
want string
|
||||
}{
|
||||
{filepath.Join(root, "Project-X", "archive"), "archive"},
|
||||
{filepath.Join(root, "Project-X", "archive", "Acme", "mdl"), "tables"},
|
||||
{filepath.Join(root, "Project-X", "archive", "Acme", "incoming"), "classifier"},
|
||||
{filepath.Join(root, "Project-X", "archive", "Acme", "received"), "archive"},
|
||||
{filepath.Join(root, "Project-X", "archive", "Acme", "issued"), "archive"},
|
||||
{filepath.Join(root, "Project-X", "working"), "mdedit"},
|
||||
{filepath.Join(root, "Project-X", "working", "alice@example.com"), "mdedit"},
|
||||
{filepath.Join(root, "Project-X", "staging"), "transmittal"},
|
||||
{filepath.Join(root, "Project-X", "reviewing"), "mdedit"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
got := DefaultToolAt(root, tc.path)
|
||||
if got != tc.want {
|
||||
t.Errorf("DefaultToolAt(%q) = %q, want %q",
|
||||
tc.path[len(root):], got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestAutoOwnAt_FromEmbeddedConvention — auto_own should be true for
|
||||
// working/incoming/staging (per the convention) and false elsewhere.
|
||||
func TestAutoOwnAt_FromEmbeddedConvention(t *testing.T) {
|
||||
resetCache()
|
||||
root := t.TempDir()
|
||||
cases := []struct {
|
||||
path string
|
||||
want bool
|
||||
}{
|
||||
{filepath.Join(root, "Project-X", "working"), true},
|
||||
{filepath.Join(root, "Project-X", "working", "alice@example.com"), true},
|
||||
{filepath.Join(root, "Project-X", "staging"), true},
|
||||
{filepath.Join(root, "Project-X", "archive", "Acme", "incoming"), true},
|
||||
{filepath.Join(root, "Project-X", "archive", "Acme", "received"), false},
|
||||
{filepath.Join(root, "Project-X", "archive", "Acme", "issued"), false},
|
||||
{filepath.Join(root, "Project-X", "archive", "Acme", "mdl"), false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
got := AutoOwnAt(root, tc.path)
|
||||
if got != tc.want {
|
||||
t.Errorf("AutoOwnAt(%q) = %v, want %v",
|
||||
tc.path[len(root):], got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestVirtualAt_FromEmbeddedConvention — reviewing/ and mdl/ are
|
||||
// declared virtual; everything else (including working/staging/
|
||||
// incoming) materialises on disk.
|
||||
func TestVirtualAt_FromEmbeddedConvention(t *testing.T) {
|
||||
resetCache()
|
||||
root := t.TempDir()
|
||||
cases := []struct {
|
||||
path string
|
||||
want bool
|
||||
}{
|
||||
{filepath.Join(root, "Project-X", "reviewing"), true},
|
||||
{filepath.Join(root, "Project-X", "archive", "Acme", "mdl"), true},
|
||||
{filepath.Join(root, "Project-X", "working"), false},
|
||||
{filepath.Join(root, "Project-X", "staging"), false},
|
||||
{filepath.Join(root, "Project-X", "archive", "Acme", "incoming"), false},
|
||||
{filepath.Join(root, "Project-X", "archive", "Acme", "received"), false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
got := VirtualAt(root, tc.path)
|
||||
if got != tc.want {
|
||||
t.Errorf("VirtualAt(%q) = %v, want %v",
|
||||
tc.path[len(root):], got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestIsDeclaredPath_FromEmbeddedConvention — canonical paths under
|
||||
// the convention are declared even on a fresh root; arbitrary paths
|
||||
// are not.
|
||||
func TestIsDeclaredPath_FromEmbeddedConvention(t *testing.T) {
|
||||
resetCache()
|
||||
root := t.TempDir()
|
||||
cases := []struct {
|
||||
path string
|
||||
want bool
|
||||
}{
|
||||
{filepath.Join(root, "Project-X", "archive"), true},
|
||||
{filepath.Join(root, "Project-X", "archive", "Acme", "incoming"), true},
|
||||
{filepath.Join(root, "Project-X", "working"), true},
|
||||
{filepath.Join(root, "Project-X", "reviewing"), true},
|
||||
{filepath.Join(root, "Project-X", "junk"), false}, // not in convention
|
||||
}
|
||||
for _, tc := range cases {
|
||||
got := IsDeclaredPath(root, tc.path)
|
||||
if got != tc.want {
|
||||
t.Errorf("IsDeclaredPath(%q) = %v, want %v",
|
||||
tc.path[len(root):], got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestChildrenDeclaredAt_FromEmbeddedConvention — at a project
|
||||
// root, the four canonical children should be enumerated.
|
||||
func TestChildrenDeclaredAt_FromEmbeddedConvention(t *testing.T) {
|
||||
resetCache()
|
||||
root := t.TempDir()
|
||||
got := ChildrenDeclaredAt(root, filepath.Join(root, "Project-X"))
|
||||
want := map[string]bool{
|
||||
"archive": true, "working": true, "staging": true, "reviewing": true,
|
||||
}
|
||||
if len(got) != len(want) {
|
||||
t.Errorf("ChildrenDeclaredAt = %v, want exactly %v keys", got, want)
|
||||
}
|
||||
for _, n := range got {
|
||||
if !want[n] {
|
||||
t.Errorf("unexpected child %q", n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestOperatorOverride_DefaultsAreSurfaceable — operator can override
|
||||
// any of the canonical tool defaults by mirroring the structure in an
|
||||
// on-disk .zddc. The override wins.
|
||||
func TestOperatorOverride_DefaultsAreSurfaceable(t *testing.T) {
|
||||
resetCache()
|
||||
root := t.TempDir()
|
||||
if err := os.MkdirAll(filepath.Join(root, "Special", "working"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Operator declares that Special/working uses classifier
|
||||
// instead of the embedded-default mdedit.
|
||||
writeZddc(t, filepath.Join(root, "Special", "working"),
|
||||
"default_tool: classifier\n")
|
||||
|
||||
if got := DefaultToolAt(root, filepath.Join(root, "Special", "working")); got != "classifier" {
|
||||
t.Errorf("operator override should set default_tool=classifier, got %q", got)
|
||||
}
|
||||
// Default still applies at other projects.
|
||||
if got := DefaultToolAt(root, filepath.Join(root, "Project-Y", "working")); got != "mdedit" {
|
||||
t.Errorf("default convention should hold at unchanged paths, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestInheritFalse_BlocksEmbeddedDefaults — at the on-disk root,
|
||||
// inherit:false stops the embedded layer from contributing. The
|
||||
// canonical paths are then no longer declared.
|
||||
func TestInheritFalse_BlocksEmbeddedDefaults(t *testing.T) {
|
||||
resetCache()
|
||||
root := t.TempDir()
|
||||
writeZddc(t, root, "inherit: false\n")
|
||||
// Without the embedded defaults' paths: tree, IsDeclaredPath
|
||||
// returns false for previously-canonical paths.
|
||||
if IsDeclaredPath(root, filepath.Join(root, "Project-X", "archive")) {
|
||||
t.Errorf("with inherit:false at root, archive should not be a declared path")
|
||||
}
|
||||
if DefaultToolAt(root, filepath.Join(root, "Project-X", "working")) != "" {
|
||||
t.Errorf("with inherit:false at root, default_tool should be empty for working")
|
||||
}
|
||||
}
|
||||
|
|
@ -66,6 +66,15 @@ func mergeOverlay(base, top ZddcFile) ZddcFile {
|
|||
if top.Inherit != nil {
|
||||
out.Inherit = top.Inherit
|
||||
}
|
||||
if top.DefaultTool != "" {
|
||||
out.DefaultTool = top.DefaultTool
|
||||
}
|
||||
if top.AutoOwn != nil {
|
||||
out.AutoOwn = top.AutoOwn
|
||||
}
|
||||
if top.Virtual != nil {
|
||||
out.Virtual = top.Virtual
|
||||
}
|
||||
|
||||
out.Admins = mergeStringSlice(out.Admins, top.Admins)
|
||||
out.ACL.Allow = mergeStringSlice(out.ACL.Allow, top.ACL.Allow)
|
||||
|
|
|
|||
Loading…
Reference in a new issue