feat(server): cascade-resolved display: labels for the canonical project peers

A directory's display: map (on-disk child name → friendly label) was read
only from the immediate on-disk .zddc, so the baked-in defaults could never
supply labels. Resolve it through the cascade instead (new zddc.DisplayAt:
embedded baseline + ancestor + on-disk overrides, deepest wins per key) and
declare the labels in the embedded project-level default
(defaults/_any_/.zddc):

  archive→Archive, incoming→Incoming, working→Working, staging→Staging,
  reviewing→Reviewing, mdl→"Master Deliverables List", rsk→"Risk Register",
  ssr→"Supplier/Subcontractor Status Report".

On-disk names stay simple/lowercase; clients render display_name in their
place (browse already does). An operator's on-disk display: still wins per
key. Drops the now-unused readDisplayMap (folded into DisplayAt). Verified
in a containerized browser: /Proj/ shows all eight friendly labels, with
mdl/rsk/ssr still rendered as click-to-table leaves.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZDDC 2026-06-05 17:48:46 -05:00
parent 18d3aaebf0
commit 7f5a54f845
3 changed files with 55 additions and 22 deletions

View file

@ -80,9 +80,13 @@ func ListDirectory(ctx context.Context, decider policy.Decider, fsRoot, dirPath,
result := make([]listing.FileInfo, 0, len(entries))
// Display overrides for this directory's children, sourced from
// THIS directory's .zddc `display:` map. Built once and looked up
// case-insensitively per entry. Empty map = no overrides.
displayMap := readDisplayMap(absDir)
// THIS directory's cascade-resolved `display:` map (embedded defaults +
// ancestor + on-disk overrides). Built once and looked up case-
// insensitively per entry. Empty/nil = no overrides. Cascade-resolved
// (not just the on-disk file) so the baked-in default labels — archive →
// "Archive", mdl → "Master Deliverables List", … — render with no
// per-project config, while an operator's on-disk display: still wins.
displayMap := zddc.DisplayAt(fsRoot, absDir)
// Set of cascade-declared child names (lowercase) for this dir.
// Entries with a matching name get Declared=true so clients can
@ -396,25 +400,6 @@ func virtualCanonicalFolders(ctx context.Context, decider policy.Decider, fsRoot
return synth
}
// readDisplayMap parses dirAbs/.zddc and returns its Display map (or
// nil when the file doesn't exist or has no display block). All keys
// are case-folded to lowercase so lookupDisplay's case-insensitive
// match is a simple map read.
func readDisplayMap(dirAbs string) map[string]string {
zf, err := zddc.ParseFile(filepath.Join(dirAbs, ".zddc"))
if err != nil || len(zf.Display) == 0 {
return nil
}
out := make(map[string]string, len(zf.Display))
for k, v := range zf.Display {
if v == "" {
continue
}
out[strings.ToLower(strings.TrimSpace(k))] = v
}
return out
}
// lookupDisplay returns the custom display label for name (matched
// case-insensitively against displayMap's keys), or "" when no
// override applies.

View file

@ -5,3 +5,17 @@ acl:
project_team: r
observer: r
document_controller: rw
# Friendly labels for the canonical project peers. On-disk names stay
# simple/lowercase; clients render these (listing display_name) in their
# place. Cascade-merged + per-key overridable, so an operator can rename
# any label in an on-disk project .zddc without renaming the folder.
display:
archive: Archive
incoming: Incoming
working: Working
staging: Staging
reviewing: Reviewing
mdl: Master Deliverables List
rsk: Risk Register
ssr: "Supplier/Subcontractor Status Report"

View file

@ -31,6 +31,40 @@ func DefaultToolAt(fsRoot, dirPath string) string {
return chain.Embedded.DefaultTool
}
// DisplayAt returns the cascade-resolved `display:` map for a directory —
// the human-friendly labels a client renders in place of on-disk child
// names (e.g. mdl → "Master Deliverables List"). Unlike a single tool
// name, display: is a MAP that merges across the cascade: the embedded
// baseline is the floor, then each on-disk level overrides per key
// (shallow→deep, so the deepest .zddc wins). Keys are lower-cased so the
// caller's lookup is case-insensitive on the on-disk basename. Returns nil
// when nothing declares a label. Mirrors how walker.go merges Display, but
// resolved on demand for one path (the listing reads it per directory).
func DisplayAt(fsRoot, dirPath string) map[string]string {
chain, err := EffectivePolicy(fsRoot, dirPath)
if err != nil {
return nil
}
merged := map[string]string{}
add := func(m map[string]string) {
for k, v := range m {
v = strings.TrimSpace(v)
if v == "" {
continue
}
merged[strings.ToLower(strings.TrimSpace(k))] = v
}
}
add(chain.Embedded.Display) // embedded baseline (the defaults tree)
for i := 0; i < len(chain.Levels); i++ {
add(chain.Levels[i].Display) // on-disk overrides, shallow→deep
}
if len(merged) == 0 {
return nil
}
return merged
}
// DirToolAt returns the cascade-resolved tool name served at the
// directory's TRAILING-SLASH URL form. Walks chain.Levels leaf→root
// (then the embedded defaults), returning the first non-empty