feat(fileapi): mirror staging transmittal folders into working/

When a folder is created under <project>/staging/ whose name parses as a
ZDDC transmittal folder (YYYY-MM-DD_<tracking> (<status>) - <title>) and
whose tracking number contains -TRN- or -SUB-, also create the same-
named folder under <project>/working/ as a drafting space for staff.

The mirror is one-way and one-shot: created at staging-mkdir time only.
Renames and deletions of either side are not propagated. The
transmittal client orchestrates cleanup at issue time (move files to
archive/<recipient>/issued/, then delete both staging and working
siblings) — the server stays out of that decision.

-MDL- tracking deliberately skips the mirror; MDL deliverables live in
archive/<party>/mdl/ rows, not via the working↔staging pairing.

Implementation: mirrorStagingToWorking() in fileapi.go, called after a
successful serveFileMkdir. EnsureCanonicalAncestors handles working/'s
own auto-own .zddc; the mirror folder gets its own creator-grant on top.

Six new tests cover -TRN-/-SUB- mirror, -MDL- skip, non-transmittal
name skip, deep-path skip, and idempotency over a pre-existing sibling.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZDDC 2026-05-07 09:18:08 -05:00
parent a79cfd2f88
commit 55abce3448
2 changed files with 212 additions and 0 deletions

View file

@ -581,11 +581,79 @@ func serveFileMkdir(cfg config.Config, w http.ResponseWriter, r *http.Request) {
}
}
// Staging↔working mirror: when a folder created under staging/ matches
// the ZDDC transmittal-folder grammar AND its tracking number contains
// -SUB- or -TRN-, also create the same-named folder under working/ as
// a drafting space for staff. The mirror is one-way and one-shot —
// renames or deletions of either side are not propagated.
if email != "" {
mirrorStagingToWorking(cfg, abs, email)
}
w.Header().Set("X-ZDDC-Source", "fileapi:mkdir")
w.WriteHeader(http.StatusCreated)
auditFile(r, "mkdir", cleanURL, http.StatusCreated, 0, nil)
}
// mirrorStagingToWorking creates a paired drafting folder under working/
// when newAbs is a transmittal-named folder under <project>/staging/. Best
// effort — failures are logged but do not affect the staging mkdir result.
//
// Eligibility:
// - newAbs's parent is exactly <project>/staging/ (case-fold)
// - filepath.Base(newAbs) parses as a transmittal folder
// (YYYY-MM-DD_<tracking> (<status>) - <title>)
// - tracking contains -SUB- or -TRN- (case-fold)
//
// Side effects on success:
// - <project>/working/ created if missing, with auto-own .zddc seeded
// (via EnsureCanonicalAncestors)
// - <project>/working/<sameName>/ created if missing, with its own
// auto-own .zddc (it's a child of the working/ canonical folder)
func mirrorStagingToWorking(cfg config.Config, newAbs, email string) {
rel, err := filepath.Rel(cfg.Root, newAbs)
if err != nil {
return
}
rel = filepath.ToSlash(rel)
parts := strings.Split(rel, "/")
if len(parts) != 3 {
// Mirror only fires for direct children of staging/. Deeper paths
// (staging/<name>/sub/) are user-managed.
return
}
if !strings.EqualFold(parts[1], "staging") {
return
}
name := parts[2]
_, tracking, _, _, ok := zddc.ParseTransmittalFolder(name)
if !ok || !zddc.IsTrnOrSubTracking(tracking) {
return
}
mirrorPath := filepath.Join(cfg.Root, parts[0], "working", name)
// Idempotent: skip if the working sibling already exists.
if info, err := os.Stat(mirrorPath); err == nil && info.IsDir() {
return
}
// EnsureCanonicalAncestors creates working/ (with its own auto-own .zddc)
// if missing; we then MkdirAll the mirror folder itself and seed its
// auto-own grant.
if _, err := zddc.EnsureCanonicalAncestors(cfg.Root, mirrorPath, email, 0o755); err != nil {
slog.Warn("mirror: ensure ancestors", "path", mirrorPath, "err", err)
return
}
if err := os.MkdirAll(mirrorPath, 0o755); err != nil {
slog.Warn("mirror: mkdir", "path", mirrorPath, "err", err)
return
}
if err := zddc.WriteAutoOwnZddc(mirrorPath, email); err != nil {
slog.Warn("mirror: auto-own .zddc", "path", mirrorPath, "err", err)
}
}
// auditFile emits a structured log line for each file API operation.
// AccessLogMiddleware already logs every request — this adds an
// op-tagged line so audit consumers can filter by operation without

View file

@ -7,6 +7,7 @@ import (
"encoding/hex"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"strings"
@ -663,3 +664,146 @@ func TestFileAPI_StrictMode_AncestorDenyAbsolute(t *testing.T) {
t.Fatalf("strict mode: vendor should be denied by root explicit-deny, got %d: %s", rec.Code, rec.Body.String())
}
}
// --- staging↔working mirror -------------------------------------------------
// stagingMirrorURL builds a URL-safe target path for a transmittal folder
// name with spaces and parens, mirroring how a real client would encode it.
func stagingMirrorURL(project, folder string) string {
return "/" + project + "/staging/" + url.PathEscape(folder) + "/"
}
func TestFileAPI_StagingMirror_TRN(t *testing.T) {
_, do, root := fileAPITestSetup(t, nil, nil)
folder := "2026-06-15_proj-EM-TRN-0042 (DFT) - Foundation Plans"
rec := do(http.MethodPost, stagingMirrorURL("Proj", folder), "alice@example.com", nil, map[string]string{
"X-ZDDC-Op": "mkdir",
})
if rec.Code != http.StatusCreated {
t.Fatalf("staging mkdir: want 201, got %d: %s", rec.Code, rec.Body.String())
}
// Staging side exists with auto-own.
stagingDir := filepath.Join(root, "Proj/staging", folder)
if info, err := os.Stat(stagingDir); err != nil || !info.IsDir() {
t.Fatalf("staging folder not created: err=%v", err)
}
if _, err := os.Stat(filepath.Join(stagingDir, ".zddc")); err != nil {
t.Errorf("staging auto-own .zddc missing: %v", err)
}
// Working mirror exists with auto-own.
workingDir := filepath.Join(root, "Proj/working", folder)
if info, err := os.Stat(workingDir); err != nil || !info.IsDir() {
t.Fatalf("working mirror not created: err=%v", err)
}
mirrorZ, err := os.ReadFile(filepath.Join(workingDir, ".zddc"))
if err != nil {
t.Fatalf("working mirror auto-own .zddc missing: %v", err)
}
if !strings.Contains(string(mirrorZ), "alice@example.com: rwcda") {
t.Errorf("mirror .zddc missing creator grant: %s", mirrorZ)
}
}
func TestFileAPI_StagingMirror_SUB(t *testing.T) {
_, do, root := fileAPITestSetup(t, nil, nil)
folder := "2026-07-01_vendor-EM-SUB-0017 (RSA) - Review Notes"
rec := do(http.MethodPost, stagingMirrorURL("Proj", folder), "alice@example.com", nil, map[string]string{
"X-ZDDC-Op": "mkdir",
})
if rec.Code != http.StatusCreated {
t.Fatalf("mkdir: want 201, got %d", rec.Code)
}
if _, err := os.Stat(filepath.Join(root, "Proj/working", folder)); err != nil {
t.Errorf("SUB-tracked folder should mirror; got err=%v", err)
}
}
func TestFileAPI_StagingMirror_NonTransmittalNameSkipped(t *testing.T) {
_, do, root := fileAPITestSetup(t, nil, nil)
rec := do(http.MethodPost, "/Proj/staging/scratch/", "alice@example.com", nil, map[string]string{
"X-ZDDC-Op": "mkdir",
})
if rec.Code != http.StatusCreated {
t.Fatalf("mkdir: want 201, got %d", rec.Code)
}
// staging/scratch/ exists.
if _, err := os.Stat(filepath.Join(root, "Proj/staging/scratch")); err != nil {
t.Fatalf("staging/scratch not created: %v", err)
}
// No working/ sibling — name doesn't parse as transmittal.
if _, err := os.Stat(filepath.Join(root, "Proj/working/scratch")); !os.IsNotExist(err) {
t.Errorf("non-transmittal name must NOT mirror; got err=%v", err)
}
}
func TestFileAPI_StagingMirror_MdlTrackingSkipped(t *testing.T) {
_, do, root := fileAPITestSetup(t, nil, nil)
folder := "2026-06-15_proj-EM-MDL-0001 (IFR) - Master Deliverables List"
rec := do(http.MethodPost, stagingMirrorURL("Proj", folder), "alice@example.com", nil, map[string]string{
"X-ZDDC-Op": "mkdir",
})
if rec.Code != http.StatusCreated {
t.Fatalf("mkdir: want 201, got %d", rec.Code)
}
// MDL deliverables are tracked in archive/<party>/mdl/, not via the
// working↔staging pairing — no mirror.
if _, err := os.Stat(filepath.Join(root, "Proj/working", folder)); !os.IsNotExist(err) {
t.Errorf("-MDL- tracking must NOT mirror; got err=%v", err)
}
}
func TestFileAPI_StagingMirror_DeepPathSkipped(t *testing.T) {
_, do, root := fileAPITestSetup(t, nil, nil)
// mkdir of staging/<name>/sub/ (depth 4) — only depth-3 (immediate
// child of staging/) qualifies for mirroring.
folder := "2026-06-15_proj-EM-TRN-0042 (DFT) - x"
if err := os.MkdirAll(filepath.Join(root, "Proj/staging", folder), 0o755); err != nil {
t.Fatal(err)
}
rec := do(http.MethodPost, "/Proj/staging/"+url.PathEscape(folder)+"/sub/", "alice@example.com", nil, map[string]string{
"X-ZDDC-Op": "mkdir",
})
if rec.Code != http.StatusCreated && rec.Code != http.StatusOK {
t.Fatalf("deep mkdir: got %d: %s", rec.Code, rec.Body.String())
}
// The transmittal folder did not get a mirror retroactively because
// the mirror only fires on depth-3 mkdirs.
if _, err := os.Stat(filepath.Join(root, "Proj/working", folder)); !os.IsNotExist(err) {
t.Errorf("deep mkdir should not retroactively mirror; got err=%v", err)
}
}
func TestFileAPI_StagingMirror_Idempotent(t *testing.T) {
_, do, root := fileAPITestSetup(t, nil, nil)
// Pre-create the working sibling with a sentinel file so we can detect
// if the mirror code blew it away.
folder := "2026-06-15_proj-EM-TRN-0042 (DFT) - existing"
mirrorDir := filepath.Join(root, "Proj/working", folder)
if err := os.MkdirAll(mirrorDir, 0o755); err != nil {
t.Fatal(err)
}
sentinel := filepath.Join(mirrorDir, "preexisting.md")
if err := os.WriteFile(sentinel, []byte("user content"), 0o644); err != nil {
t.Fatal(err)
}
rec := do(http.MethodPost, stagingMirrorURL("Proj", folder), "alice@example.com", nil, map[string]string{
"X-ZDDC-Op": "mkdir",
})
if rec.Code != http.StatusCreated {
t.Fatalf("mkdir: want 201, got %d", rec.Code)
}
// Sentinel still exists — mirror was idempotent (no-op when sibling
// already present).
if _, err := os.Stat(sentinel); err != nil {
t.Errorf("idempotency: pre-existing content gone: %v", err)
}
}