ZDDC/zddc/internal/zddc/defaults.zddc.yaml
ZDDC 690d185dc2 feat: reviewing/ lifecycle — Plan Review endpoint, virtual received window, browse context-menu workflows
Two layers shipped together since the second builds on the first.

LAYER 1 — reviewing/ + Plan Review scaffolding

- reviewing/ is now a real folder under each project, populated by the
  Plan Review composite endpoint. The old reviewing/ virtual aggregator
  handler is retired.
- POST /<project>/archive/<party>/received/<tracking>/ with X-ZDDC-Op:
  plan-review scaffolds physical workflow folders under reviewing_root
  and staging_root, each carrying .zddc.received_path pointing back at
  the canonical submittal. Idempotent re-runs match by received_path
  and re-converge the ACL.
- Virtual received window: when listing or writing under
  <workflow>/received/, the server resolves through the canonical
  archive/<party>/received/<tracking>/ via the workflow's
  .zddc.received_path. Writes get rewritten to
  <workflow>/<base>+C<n><suffix> so review comments land in the
  workflow folder and never touch the WORM archive.
- Cascade defaults declare on_plan_review per project so the
  reviewing_root and staging_root are configurable.

LAYER 2 — browse context-menu workflows

- Accept Transmittal: right-click a transmittal folder in
  archive/<party>/incoming/ → validates ZDDC folder + filename
  conformance, atomic-renames the folder to
  archive/<party>/received/<tracking>/ (WORM zone), and optionally
  chains into Plan Review in the same composite request. Re-acceptance
  with a different revision merges file-by-file; WORM forbids
  overwrite of an existing filename.
- Stage / Unstage: right-click files in working/<…>/ → "Stage to…"
  with picker of existing staging transmittal folders + inline
  "New transmittal folder…" create; right-click files in
  staging/<…>/ → "Unstage to working/" defaulting to the user's
  working/<email>/ home. Reuses the file-API move primitive.
- Create Transmittal folder: right-click the staging/ pane → prompts
  for a ZDDC-conforming folder name with live validation; mkdir,
  then navigate to the new folder URL where the transmittal tool
  serves the editor.
- Supporting infrastructure: new CanonicalFolderAt cascade lookup +
  X-ZDDC-Canonical-Folder response header so the browse SPA can
  scope-gate menu items without re-implementing the cascade
  client-side.

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

256 lines
12 KiB
YAML

# defaults.zddc — embedded baseline configuration for every ZDDC
# deployment. Baked into the binary via //go:embed in defaults.go,
# loaded as the bottom-most level of the cascade. Operators override
# at the on-disk root /.zddc (or any deeper level); to ignore this
# file entirely, set `inherit: false` on an on-disk .zddc.
#
# 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 (both contribute to the cascade, on-disk wins per-field).
title: "ZDDC"
# 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: {}
# ── Standard roles ─────────────────────────────────────────────────────────
#
# Two roles ship empty (no members) — a fresh deployment grants
# nothing until an operator populates them. They're referenced by the
# project-scoped grants in paths: below.
#
# Role membership UNIONS across the cascade: an on-disk .zddc that
# defines `project_team` again with one extra member ADDS that member
# to the inherited role. To start fresh at a subtree (e.g. a project
# wanting its own team independent of a deployment-wide default), use
# `reset: true` on the role at that level — ancestor definitions above
# the reset are then excluded.
#
# document_controller — the people who file into archive/<party>/
# received/ and issued/ (WORM zones). They get read+write-once-
# create there (via the worm: lists below) and read/write
# elsewhere in a project, plus subtree-admin of working/ and
# staging/ so they can stand up new top-level folders and manage
# user/staging subtrees. They are NOT subtree-admin of archive/,
# so the WORM constraint still binds them in received/issued.
#
# project_team — everyone working on a project. Read-only across
# the project. Their own working/<email>/ home and anything they
# create under incoming/ get a creator-owned auto-own .zddc
# (rwcda) which wins via deepest-match, so "read-only except
# what I own" falls out of the cascade with no special rule.
roles:
document_controller:
members: []
project_team:
members: []
# Universal tool baseline. archive (record browser), browse (file
# tree, hosts the in-place markdown editor), and landing (project
# picker) work everywhere. Each canonical folder below adds its own
# context-specific tools (transmittal in staging/, etc.). The cascade
# unions available_tools across all levels — leaf restrictions don't
# drop ancestor entries — so this baseline propagates to every
# descendant.
available_tools: [archive, browse, landing]
# ── The slash / no-slash routing convention ────────────────────────────────
#
# Every directory URL has two forms, each served by a configurable
# tool:
#
# <dir>/ (trailing slash) → `dir_tool` — the directory view.
# Defaults to `browse` (file-tree
# navigator). This is the site-wide
# default; you rarely set it.
# <dir> (no slash) → `default_tool` — the "specialized
# app" for this folder (e.g. archive,
# transmittal, tables). If a folder
# declares no default_tool, the no-
# slash form just 302s to the slash
# form, so you land on `dir_tool`.
#
# JSON listing requests are unaffected by either key — they always get
# the raw directory listing, so the browse SPA (and any other client)
# can enumerate entries no matter what dir_tool/default_tool are.
#
# Both keys cascade leaf→root: a parent's default_tool applies to
# descendants unless a deeper level overrides it (browse set on
# working/ reaches working/alice/notes/ for free). The keys below set
# default_tool on the canonical folders; dir_tool is left unset
# everywhere, so the slash form is always `browse`.
#
# ── 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
# (Plan-Review-managed draft workspaces). 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.
"*":
# Project-scoped baseline ACL. project_team gets read across the
# project; document_controller gets read + overwrite-existing
# (so people can ask them to fix a stuck file). Neither gets
# `c` (create) at this level — that's granted only at the
# specific spots below (archive/, working/, staging/), so the
# doc controller can't make arbitrary folders. Grants here cap
# at deeper levels per deepest-match-wins, except where a deeper
# .zddc restates a fuller grant for the same principal.
acl:
permissions:
project_team: r
document_controller: rw
# Plan Review composite endpoint: the doc controller right-clicks
# archive/<party>/received/<tracking>/ in the browse app and gets
# a "Plan Review" item that scaffolds workflow folders under the
# paths below. Both keys required; omitting the block disables
# the menu item for this subtree.
on_plan_review:
reviewing_root: reviewing/
staging_root: staging/
paths:
archive:
default_tool: archive
# The doc controller can create party subfolders here
# (archive/<party>/). Restate the full grant — deepest-match
# is per-principal replacement, so we re-list rw + add c.
acl:
permissions:
document_controller: rwc
paths:
# Second segment under archive/ is the party name.
"*":
# When the doc controller creates a party folder, an
# auto-own .zddc grants them rwcda there (UNFENCED — so
# the project-level project_team:r still cascades through
# to received/issued). That lets them set up the
# counterparty's own .zddc afterward.
auto_own: true
paths:
mdl:
default_tool: tables
available_tools: [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:
# incoming/ is the COUNTERPARTY's drop zone. The flow:
# 1. the other party's document controller uploads
# files here (a deployment grants them cr at this
# path, e.g. acl: { permissions: { "*@acme.com": cr } }
# at archive/Acme/incoming/.zddc — or they mkdir a
# dated subfolder under incoming/ and own it via
# auto_own)
# 2. OUR document controller QCs them via classifier
# (rename in place) and moves them to received/
# (which needs delete here + worm-create there),
# ideally returning a signed transmittal in issued/
#
# The normal project_team has only read here (inherited
# from the project level — they have no c/w) so they can
# see what's been dropped but not touch it. The
# document_controller grant restates rwcd so the QC +
# transfer-out workflow has the delete bit it needs.
default_tool: classifier
available_tools: [classifier]
auto_own: true
drop_target: true
acl:
permissions:
document_controller: rwcd
# received/ and issued/ are WORM (write-once-read-many).
# The `worm:` list marks the zone:
#
# - write (w) and delete (d) are stripped for EVERYONE
# - create (c) is stripped for everyone EXCEPT the
# principals listed — they get read + write-once-
# create ("cr")
# - read for non-listed principals is whatever the
# normal cascade ACL granted; the WORM list does not
# itself confer read to outsiders
# - admins (root / subtree) bypass entirely — the
# human escape hatch for mis-filed documents
#
# The baseline is an empty list: WORM zone, no
# create-capable principals — filing is locked until a
# deployment names a document controller, e.g.
#
# worm: ["doc-control@example.com"]
#
# at received/ (or issued/, or archive/<party>/, or
# wherever scopes it right). worm: lists UNION across the
# cascade, so a deeper .zddc adds more controllers.
received:
default_tool: archive
# document_controller may file write-once into the
# WORM zone. Their project-level rw is masked here
# to r; worm: restores write-once-create.
worm: [document_controller]
issued:
default_tool: archive
worm: [document_controller]
working:
default_tool: browse
available_tools: [browse, classifier]
# working/ auto-owns the first creator + the per-user homes
# below.
auto_own: true
drop_target: true
# Doc controller is subtree-admin of working/ — full create
# + manage, including taking over a fenced per-user home if a
# user leaves. (Scoped here, not at the project root, so the
# WORM constraint in archive/<party>/received|issued still
# binds them.)
admins: [document_controller]
paths:
"*": # per-user home dir
default_tool: browse
available_tools: [browse, classifier]
auto_own: true
# Per-user home is private by default: the generated
# auto-own .zddc carries inherit:false so ancestor ACL
# grants don't reach inside. The user can edit the file
# to grant collaborators access.
auto_own_fenced: true
drop_target: true
staging:
default_tool: transmittal
available_tools: [transmittal, classifier]
auto_own: true
drop_target: true
# Doc controller is subtree-admin of staging/ too — same
# rationale as working/.
admins: [document_controller]
reviewing:
default_tool: browse
available_tools: [browse]
# reviewing/ is the doc-controller's draft-workspace area. The
# "Plan Review" composite endpoint (see on_plan_review at project
# level) scaffolds a physical folder here for each submittal
# under review, with a .zddc carrying received_path back to the
# canonical submittal in received/. Subtree-admin so the doc
# controller can author per-folder .zddc files (originator ACL,
# planned_date).
auto_own: true
drop_target: true
admins: [document_controller]