May 2026 reshape. archive/ is now the only physical project-root
directory; working/, staging/, reviewing/ move from the project root
into each archive/<party>/ folder. Six top-level URLs become virtual
aggregators served via the cascade rather than disk:
ssr/mdl/rsk tables rollups across parties with a
synthesised $party source-party column
working/staging/ browse folder-nav listings of parties with
reviewing non-empty content in the slot; per-party
URLs 302-redirect to archive/<party>/<slot>/
Mkdir at the project root is restricted to `archive` and `_`/`.`-
prefixed system names — virtual aggregator names and ad-hoc folders
return 409.
Plan Review hardcodes the scaffold convention (archive/<party>/
{reviewing,staging}/<tracking>/); the pre-reshape
on_plan_review.{reviewing_root,staging_root} cascade keys are dropped.
document_controller is now subtree-admin of every archive/<party>/
(not of project-root working/staging/ as before), so per-party
lifecycle slots inherit admin authority through the cascade.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
100 lines
4.3 KiB
Go
100 lines
4.3 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), under the
|
|
// May 2026 reshape:
|
|
//
|
|
// - <project>/archive/<party>/{mdl,rsk}/... → "tables"
|
|
// - <project>/archive/<party>/staging/... → "transmittal"
|
|
// - <project>/archive/<party>/{working,reviewing}/...
|
|
// → "browse" (hosts the
|
|
// markdown editor plugin)
|
|
// - <project>/archive/<party>/incoming/... → "classifier"
|
|
// - <project>/archive/<party>/{received,issued}/...
|
|
// → "archive"
|
|
// - <project>/archive/ → "archive"
|
|
// - <project>/{ssr,mdl,rsk} → "tables" (project-
|
|
// level rollup virtuals
|
|
// with synthesized
|
|
// $party column)
|
|
// - <project>/{working,staging,reviewing} → "browse" (project-
|
|
// level folder-nav
|
|
// virtuals — per-party
|
|
// URLs 302 to the
|
|
// canonical archive/
|
|
// <party>/<slot>/)
|
|
// - any other directory → "" (no default)
|
|
//
|
|
// The {mdl,rsk} 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)
|
|
}
|