ZDDC/zddc/internal/apps/availability.go
ZDDC 5e393cbeaf feat(zddc): Phase 3 completion — all canonical-folder behaviour now cascade-driven
Final consumer migration. The Go-coded lists that previously encoded
the ZDDC convention all defer to the .zddc cascade now.

Schema added:
  available_tools: [tool1, tool2, ...]   concat-union across cascade;
                                          tools not in the union are
                                          denied auto-route at that path
  auto_own_fenced: true|false             generated auto-own .zddc
                                          carries inherit:false (private
                                          to creator)

Lookups added:
  AvailableToolsAt(root, dir)   union of available_tools across cascade
  IsToolAvailableAt(root, dir, tool)
  AutoOwnFencedAt(root, dir)    leaf-only

Cascade semantics finalised (per field):
  default_tool      → leaf→root walk (parent applies to descendants)
  available_tools   → leaf→root union (each level adds; baseline at root)
  auto_own          → leaf-only (creating THIS dir specifically)
  auto_own_fenced   → leaf-only (same)
  virtual           → leaf-only (THIS dir is virtual, not subtree)

Consumers migrated:
  apps.DefaultAppAt        → zddc.DefaultToolAt
  apps.AppAvailableAt      → zddc.IsToolAvailableAt (+ landing special)
  EnsureCanonicalAncestors → AutoOwnAt + AutoOwnFencedAt
  fs.ListDirectory empty-list fallback     → zddc.IsDeclaredPath
  fs.virtualCanonicalFolders               → zddc.ChildrenDeclaredAt
  dispatcher canonical-folder branches     → unified into one
                                              cascade-declared block

Hardcoded helpers REMOVED (dead code):
  apps.inAncestorWithName
  zddc.autoOwnDepthMatch / isAutoOwnDepthMatch

Hardcoded lists kept as data sources for the cascade walker but
no longer drive routing logic:
  ProjectRootFolders / PartyFolders / AutoOwnCanonicalNames /
  VirtualOnlyCanonicalNames / IsProjectRootFolder / IsArchivePartyFolder /
  IsArchivePartyMdlDir — all still defined; only `ProjectRootFolders`
  is used by special.go's IsProjectRootFolder. The rest are dead.

Dispatcher unified: the previously-two branches (per-party folder vs
project-root folder) collapse into one cascade-declared-path block
that handles the slash/no-slash convention uniformly:
  - no-slash, default_tool=tables  → ServeTable (default-MDL fallback)
  - no-slash, default_tool set     → apps.Serve(tool)
  - no-slash, no default_tool      → 302 to slash form
  - slash, any                     → ServeDirectory empty-list fallback

The IsDir branch's switch also un-hardcoded — any cascade tool is
served (not just the legacy 3 names), so e.g. /Project/archive/<party>
/incoming (no slash) now serves classifier directly rather than 302'ing
to the slash form.

defaults.zddc.yaml populated with the canonical convention as the
recipe. Operators edit it (or override per-directory on disk) to
change any behaviour — no Go code changes required.

Browse drag-drop scope (working/staging/incoming) is the one remaining
client-side hardcoded regex; cascading that requires the cascade JSON
to be served to the client, which is its own Phase 4 piece.

Tests updated for the new no-slash mdl URL convention (landing MDL
card test) and no-slash stage URLs (nav strip test). All 248
Playwright + all Go tests green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:36:33 -05:00

86 lines
3.4 KiB
Go

package apps
import (
"path/filepath"
"strings"
"codeberg.org/VARASYS/ZDDC/zddc/internal/zddc"
)
// AppAvailableAt reports whether app's virtual HTML can be served at
// requestDir. Delegates to the .zddc cascade's available_tools union
// (zddc.IsToolAvailableAt). The convention previously hardcoded here
// now lives in defaults.zddc.yaml and is overridable per-directory
// by operators.
//
// Operators can always drop a real <name>.html file at any path to
// override — that path is served by the static handler regardless of
// this function's result. AppAvailableAt is consulted only when no
// real file exists.
//
// Landing is a special case: the cascade declares it available
// universally (since available_tools concat-merges from the root
// baseline), but the dispatcher only auto-serves landing at the
// deployment root. We enforce that here too so callers don't trip
// over project-deep landing requests.
func AppAvailableAt(root, requestDir, app string) bool {
root = filepath.Clean(root)
requestDir = filepath.Clean(requestDir)
if app == "landing" {
return requestDir == root
}
return zddc.IsToolAvailableAt(root, requestDir, app)
}
// DefaultAppAt returns the canonical default tool name for requestDir,
// or "" if no specific tool fits. Used by the dispatcher to decide
// which app to serve at a directory URL with no trailing slash —
// trailing-slash URLs serve the browse app for any directory.
//
// Rules (case-insensitive on canonical folder names):
//
// - <project>/archive/<party>/mdl/... → "tables"
// - <project>/archive/ → "archive"
// - <project>/archive/<party>/... → "archive"
// - <project>/staging/... → "transmittal"
// - <project>/working/... → "mdedit"
// - <project>/reviewing/... → "mdedit" (operates on the
// virtual aggregator listing)
// - any other directory → "" (no default)
//
// The mdl rule wins over the broader archive rule because the table
// editor is a more specific surface for browsing planned deliverables
// than the archive index. Note: the dir at archive/<party>/mdl/
// itself IS the table — its table.yaml + form.yaml + row YAMLs all
// live there together (self-contained directory).
//
// requestDir and root are absolute filesystem paths; requestDir must
// be under root (otherwise "" is returned).
//
// Phase 3b: delegates to zddc.DefaultToolAt, which resolves the
// answer from the .zddc cascade (operator on-disk + embedded
// defaults). The convention previously hardcoded in the switch
// statement below now lives in zddc/internal/zddc/defaults.zddc.yaml
// and is overridable per-directory by operators.
//
// Project root itself (depth-1) still returns "" — the cascade
// doesn't declare a default tool for it (landing is handled by the
// dispatcher's own depth-1 check).
func DefaultAppAt(root, requestDir string) string {
root = filepath.Clean(root)
requestDir = filepath.Clean(requestDir)
if requestDir == root {
return ""
}
rel, err := filepath.Rel(root, requestDir)
if err != nil || strings.HasPrefix(rel, "..") {
return ""
}
parts := strings.Split(rel, string(filepath.Separator))
if len(parts) < 2 {
// Project root itself — no default tool from the cascade.
// (Landing is handled separately by the dispatcher.)
return ""
}
return zddc.DefaultToolAt(root, requestDir)
}