feat(zddc): retire defaults.zddc.yaml; .zddc.zip is the policy carrier (phase 6)

Completes the migration. The embedded per-depth tree (internal/zddc/defaults/)
is now the sole source of the shipped baseline; defaults.zddc.yaml is deleted.

  - EmbeddedDefaults() assembles the tree (no yaml). show-defaults now emits a
    .zddc.zip (per-depth, "*" wildcard members) via EmbeddedDefaultsZip() —
    operators redirect it to <ROOT>/.zddc.zip (or any directory) and edit/add/
    delete individual members.
  - Dropped EmbeddedDefaultsBytes; reworked the dumpable test to validate the
    emitted zip; removed the now-redundant tree-vs-yaml oracle (the Layer-2
    matrix is the ongoing behavioral guarantee, and it stays green).
  - Swept stale "defaults.zddc.yaml" comment references to the embedded tree.
  - GRAMMAR.md §1/§6 updated: .zddc.zip is a policy bundle mountable at ANY
    directory (subtree mount; inherit:false + acl.inherit:false = island); the
    shipped baseline is the embedded bundle at the root.

Net of the 6-phase migration: policy is per-depth .zddc files in a .zddc.zip
that an operator can drop at any level to override the cascade; the engine
(Assemble + the unchanged walker) enforces it. Full Go suite + matrix green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZDDC 2026-06-05 11:35:21 -05:00
parent 4681f2c358
commit 1e0e403f1e
24 changed files with 134 additions and 410 deletions

View file

@ -5,8 +5,9 @@ its type, how it composes across the cascade, and how the engine turns a tree of
`.zddc` files into an access decision. The design intent is **policy-as-data**: `.zddc` files into an access decision. The design intent is **policy-as-data**:
operators express *all* per-folder behaviour in `.zddc`; `zddc-server` is the operators express *all* per-folder behaviour in `.zddc`; `zddc-server` is the
enforcement engine that reads and applies it. Nothing here is hardcoded to enforcement engine that reads and applies it. Nothing here is hardcoded to
folder *names* — the embedded defaults (`internal/zddc/defaults.zddc.yaml`) are folder *names* — the embedded defaults (the per-depth tree under
just the bottom-most `.zddc` in the cascade and are fully overridable. `internal/zddc/defaults/`, exported as a `.zddc.zip` by `show-defaults`) are
just the bottom-most policy in the cascade and are fully overridable.
> Status: the declarative grammar below is complete and enforced today. A > Status: the declarative grammar below is complete and enforced today. A
> future **sandboxed expression layer** (`when:` conditions — see > future **sandboxed expression layer** (`when:` conditions — see
@ -30,14 +31,20 @@ ordered chain of `.zddc` files from the deployment root down to that directory,
plus the embedded defaults at the bottom. Levels are indexed **root (0) → leaf plus the embedded defaults at the bottom. Levels are indexed **root (0) → leaf
(last)**. (last)**.
Three things contribute to a level beyond the on-disk file: Four things contribute to a level beyond the on-disk file:
1. **Embedded defaults**`defaults.zddc.yaml`, always the bottom of the chain 1. **Embedded defaults** — the per-depth tree under `internal/zddc/defaults/`
(assembled into a nested `paths:` `ZddcFile`), always the bottom of the chain
(unless fenced off; see [`inherit`](#inherit)). (unless fenced off; see [`inherit`](#inherit)).
2. **Virtual `paths:` contributions** — an ancestor's [`paths:`](#paths) tree 2. **`.zddc.zip` policy bundles** — a `.zddc.zip` at *any* directory mounts a
policy subtree there: its members (paths with `*` wildcards) are resolved
like `paths:` and merged UNDER the on-disk `.zddc`. With `inherit:false` +
`acl.inherit:false` in its root member it becomes a self-contained island.
The embedded defaults are simply the bundle mounted at the deployment root.
3. **Virtual `paths:` contributions** — an ancestor's [`paths:`](#paths) tree
injects policy at descendant directories *that need not exist on disk*. This injects policy at descendant directories *that need not exist on disk*. This
is why a brand-new project resolves usable policy at every canonical URL. is why a brand-new project resolves usable policy at every canonical URL.
3. **The on-disk `.zddc`** at the directory itself, which wins per-field over 4. **The on-disk `.zddc`** at the directory itself, which wins per-field over
the virtual/ancestor contributions. the virtual/ancestor contributions.
Because policy is resolved for *virtual* paths, **every authorization decision Because policy is resolved for *virtual* paths, **every authorization decision
@ -168,9 +175,12 @@ folders existing on disk.
- `.zddc.d/` — per-directory admin-only reserve (tokens, logs, history, - `.zddc.d/` — per-directory admin-only reserve (tokens, logs, history,
converted cache, view configs). 404 to non-admins at every depth; writes converted cache, view configs). 404 to non-admins at every depth; writes
admin-gated. Not addressable as content. admin-gated. Not addressable as content.
- `.zddc.zip` — project-root config bundle: tool-HTML overrides today, and the - `.zddc.zip` — a config bundle droppable at **any** directory. Its `.zddc`
intended carrier for per-depth default `.zddc` files (individually members (per-depth, `*` wildcards, individually replaceable) mount a policy
replaceable) tomorrow. Browsable only by an elevated admin. subtree at that directory (see §1); it may also carry tool-HTML overrides.
The shipped baseline is the embedded bundle at the deployment root
(`show-defaults` exports it). Browsable only by an elevated admin. NOT a
cascade key — it's resolved by the engine, not declared inside a `.zddc`.
--- ---

View file

@ -52,14 +52,19 @@ func main() {
fmt.Print(policy.FederalRego) fmt.Print(policy.FederalRego)
return return
case "show-defaults", "--show-defaults": case "show-defaults", "--show-defaults":
// Dump the embedded baseline .zddc to stdout. Pipe into a // Emit the embedded baseline as a .zddc.zip (per-depth policy
// real file (e.g. $ZDDC_ROOT/.zddc) to start from the // tree, "*" wildcard members) to stdout. Redirect into a bundle
// shipped defaults and edit; the on-disk copy then // (e.g. `> $ZDDC_ROOT/.zddc.zip`) to start from the shipped
// participates in the cascade alongside the embedded // defaults and edit/add/delete individual members; the bundle
// layer (both contribute; child wins). To ignore the // participates in the cascade (child wins). Drop it at any
// embedded layer entirely after exporting, set // directory to mount a subtree; add inherit:false +
// `inherit: false` at the top of the exported file. // acl.inherit:false to fully replace the baseline there.
_, _ = os.Stdout.Write(zddc.EmbeddedDefaultsBytes()) b, err := zddc.EmbeddedDefaultsZip()
if err != nil {
fmt.Fprintln(os.Stderr, "show-defaults:", err)
os.Exit(1)
}
_, _ = os.Stdout.Write(b)
return return
} }
} }
@ -218,7 +223,7 @@ func main() {
"no_auth", cfg.NoAuth) "no_auth", cfg.NoAuth)
// Bootstrap sanity: warn loudly (but don't fail) when the root .zddc // Bootstrap sanity: warn loudly (but don't fail) when the root .zddc
// grants nobody anything. Embedded defaults.zddc.yaml ships with empty // grants nobody anything. Embedded internal/zddc/defaults/ ships with empty
// role members, so a fresh deployment refuses every request until the // role members, so a fresh deployment refuses every request until the
// operator populates the file. // operator populates the file.
warnIfNoBootstrap(cfg) warnIfNoBootstrap(cfg)
@ -516,7 +521,7 @@ func setupApps(cfg config.Config) (*apps.Server, error) {
} }
// warnIfNoBootstrap fires a startup slog.Warn when the root .zddc grants // warnIfNoBootstrap fires a startup slog.Warn when the root .zddc grants
// nobody anything — the embedded defaults.zddc.yaml ships with empty role // nobody anything — the embedded defaults ships with empty role
// members, so a deployment without operator-populated admins / acl // members, so a deployment without operator-populated admins / acl
// permissions / role members refuses every request. Skipped under // permissions / role members refuses every request. Skipped under
// --no-auth (auth disabled; warning would be redundant). Per-project // --no-auth (auth disabled; warning would be redundant). Per-project

View file

@ -10,7 +10,7 @@ import (
// AppAvailableAt reports whether app's virtual HTML can be served at // AppAvailableAt reports whether app's virtual HTML can be served at
// requestDir. Delegates to the .zddc cascade's available_tools union // requestDir. Delegates to the .zddc cascade's available_tools union
// (zddc.IsToolAvailableAt). The convention previously hardcoded here // (zddc.IsToolAvailableAt). The convention previously hardcoded here
// now lives in defaults.zddc.yaml and is overridable per-directory // now lives in internal/zddc/defaults/ and is overridable per-directory
// by operators. // by operators.
// //
// Operators can always drop a real <name>.html file at any path to // Operators can always drop a real <name>.html file at any path to
@ -74,7 +74,7 @@ func AppAvailableAt(root, requestDir, app string) bool {
// Phase 3b: delegates to zddc.DefaultToolAt, which resolves the // Phase 3b: delegates to zddc.DefaultToolAt, which resolves the
// answer from the .zddc cascade (operator on-disk + embedded // answer from the .zddc cascade (operator on-disk + embedded
// defaults). The convention previously hardcoded in the switch // defaults). The convention previously hardcoded in the switch
// statement below now lives in zddc/internal/zddc/defaults.zddc.yaml // statement below now lives in zddc/internal/zddc/defaults/
// and is overridable per-directory by operators. // and is overridable per-directory by operators.
// //
// Project root itself (depth-1) still returns "" — the cascade // Project root itself (depth-1) still returns "" — the cascade

View file

@ -63,7 +63,7 @@ func ListDirectory(ctx context.Context, decider policy.Decider, fsRoot, dirPath,
// Empty-listing fallback for cascade-declared paths. A fresh // Empty-listing fallback for cascade-declared paths. A fresh
// project doesn't have working/, staging/, reviewing/, or // project doesn't have working/, staging/, reviewing/, or
// archive/<party>/incoming/ on disk until something is // archive/<party>/incoming/ on disk until something is
// written into them — but the cascade (defaults.zddc.yaml // written into them — but the cascade (internal/zddc/defaults/
// plus any on-disk overrides) declares them via paths:, so // plus any on-disk overrides) declares them via paths:, so
// the stage-strip / file nav can link unconditionally. // the stage-strip / file nav can link unconditionally.
// Returning [] gives a usable empty view (the tables peers // Returning [] gives a usable empty view (the tables peers

View file

@ -3,7 +3,7 @@ package handler
// Layer 2 — the SHIPPED DEFAULT POLICY contract. // Layer 2 — the SHIPPED DEFAULT POLICY contract.
// //
// This is the executable truth table for the embedded defaults // This is the executable truth table for the embedded defaults
// (internal/zddc/defaults.zddc.yaml): role × canonical-path × verb → allow/deny. // (internal/zddc/defaults/): role × canonical-path × verb → allow/deny.
// It pins the document-control access model so a change to the defaults — OR to // It pins the document-control access model so a change to the defaults — OR to
// the engine that resolves them — can't silently alter who-can-do-what. (When // the engine that resolves them — can't silently alter who-can-do-what. (When
// the defaults later move into a project-root .zddc.zip of per-depth .zddc // the defaults later move into a project-root .zddc.zip of per-depth .zddc

View file

@ -807,7 +807,7 @@ func serveFileMkdir(cfg config.Config, w http.ResponseWriter, r *http.Request) {
} }
// Auto-ownership for the newly-created directory. The .zddc // Auto-ownership for the newly-created directory. The .zddc
// cascade's `auto_own:` flag (see defaults.zddc.yaml) drives this, // cascade's `auto_own:` flag (see internal/zddc/defaults/) drives this,
// same as EnsureCanonicalAncestors. A creator-owned .zddc lands // same as EnsureCanonicalAncestors. A creator-owned .zddc lands
// inside abs when: // inside abs when:
// - abs itself is declared auto_own (e.g. an explicit mkdir of // - abs itself is declared auto_own (e.g. an explicit mkdir of

View file

@ -258,7 +258,7 @@ func serveFormCreateRollup(cfg config.Config, req *FormRequest, w http.ResponseW
} }
// Resolve the cascade rule at slotAbs to pick a composed filename. // Resolve the cascade rule at slotAbs to pick a composed filename.
// The defaults.zddc.yaml records: entries declare a "*.yaml" rule // The internal/zddc/defaults/ records: entries declare a "*.yaml" rule
// for both mdl/ and rsk/ folders with filename_format pointing at // for both mdl/ and rsk/ folders with filename_format pointing at
// body fields; for RSK, the rule also carries row_field + // body fields; for RSK, the rule also carries row_field +
// row_scope_fields so the server can assign the next row sequence // row_scope_fields so the server can assign the next row sequence

View file

@ -44,7 +44,7 @@ func IsZddcFileRequest(urlPath string) bool {
// //
// Virtual: if it does not exist, the body is the cascade's // Virtual: if it does not exist, the body is the cascade's
// //
// leaf-level ZddcFile (what defaults.zddc.yaml's paths: // leaf-level ZddcFile (what internal/zddc/defaults/'s paths:
// tree declares for THIS exact directory, plus any // tree declares for THIS exact directory, plus any
// virtual contributions threaded through by the walker) // virtual contributions threaded through by the walker)
// marshalled as YAML. A header comment names the source // marshalled as YAML. A header comment names the source
@ -143,7 +143,7 @@ func ServeZddcFile(cfg config.Config, w http.ResponseWriter, r *http.Request) {
// renderVirtualZddc produces a YAML body for a directory that has no // renderVirtualZddc produces a YAML body for a directory that has no
// .zddc on disk. The body is the cascade's leaf-level ZddcFile — // .zddc on disk. The body is the cascade's leaf-level ZddcFile —
// i.e. what defaults.zddc.yaml's paths: tree declares for this exact // i.e. what internal/zddc/defaults/'s paths: tree declares for this exact
// directory, plus any contributions the walker threaded through. The // directory, plus any contributions the walker threaded through. The
// goal is to expose the embedded defaults' source of truth: a new // goal is to expose the embedded defaults' source of truth: a new
// user opening the virtual .zddc here sees, in the same yaml shape // user opening the virtual .zddc here sees, in the same yaml shape
@ -164,7 +164,7 @@ func renderVirtualZddc(chain zddc.PolicyChain) (string, error) {
var b strings.Builder var b strings.Builder
b.WriteString("# Virtual .zddc — no file on disk at this directory.\n") b.WriteString("# Virtual .zddc — no file on disk at this directory.\n")
b.WriteString("# The content below is what the embedded defaults\n") b.WriteString("# The content below is what the embedded defaults\n")
b.WriteString("# (defaults.zddc.yaml's paths: tree) declare for this\n") b.WriteString("# (internal/zddc/defaults/'s paths: tree) declare for this\n")
b.WriteString("# exact path. Edit and save through the YAML editor in\n") b.WriteString("# exact path. Edit and save through the YAML editor in\n")
b.WriteString("# browse to materialise a real .zddc here carrying your\n") b.WriteString("# browse to materialise a real .zddc here carrying your\n")
b.WriteString("# changes; the bytes you save become the override\n") b.WriteString("# changes; the bytes you save become the override\n")

View file

@ -79,7 +79,7 @@ func TestServeZddcFile_ExistingFile(t *testing.T) {
// (project_team: r, observer: r, document_controller: rw) plus the // (project_team: r, observer: r, document_controller: rw) plus the
// canonical paths: tree (archive, working, staging, reviewing, …). // canonical paths: tree (archive, working, staging, reviewing, …).
// Asserts a few load-bearing markers; the full content is the // Asserts a few load-bearing markers; the full content is the
// `defaults.zddc.yaml` source-of-truth, which lives under // `internal/zddc/defaults/` source-of-truth, which lives under
// zddc/internal/zddc and is parsed at every cascade walk. // zddc/internal/zddc and is parsed at every cascade walk.
func TestServeZddcFile_VirtualDefault(t *testing.T) { func TestServeZddcFile_VirtualDefault(t *testing.T) {
root := t.TempDir() root := t.TempDir()

View file

@ -236,7 +236,7 @@ func (d *InternalDecider) Allow(_ context.Context, input AllowInput) (bool, erro
} }
// WORM zone: a directory whose cascade declares `worm:` (see // WORM zone: a directory whose cascade declares `worm:` (see
// defaults.zddc.yaml — archive/<party>/received and issued carry // internal/zddc/defaults/ — archive/<party>/received and issued carry
// `worm: {}`) is write-locked. Inside it, the effective verbs // `worm: {}`) is write-locked. Inside it, the effective verbs
// for a non-admin principal are: // for a non-admin principal are:
// //

View file

@ -1,31 +1,58 @@
package zddc package zddc
import ( import (
"archive/zip"
"bytes"
"embed" "embed"
"io/fs"
"strings"
"sync" "sync"
) )
// defaultsBytes is the legacy single-file embedded baseline. Retained only so
// TestEmbeddedTreeMatchesYAML can prove the per-depth tree (the new source of
// truth) assembles to exactly the same ZddcFile. Removed once that guarantee
// is locked. (Still surfaced by EmbeddedDefaultsBytes / show-defaults for now.)
//
//go:embed defaults.zddc.yaml
var defaultsBytes []byte
// defaultsTreeFS is the embedded per-depth default policy tree — the source of // defaultsTreeFS is the embedded per-depth default policy tree — the source of
// truth. `all:` includes the `.zddc` (dot) files and `_any_` (underscore) // truth for the shipped baseline, the bottom of every cascade. `all:` includes
// directories that a bare //go:embed would skip. // the `.zddc` (dot) files and `_any_` (underscore) directories a bare
// //go:embed would skip. The `_any_` directory is the on-disk stand-in for the
// "*" wildcard segment (kept out of literal "*" directories in the repo).
// //
//go:embed all:defaults //go:embed all:defaults
var defaultsTreeFS embed.FS var defaultsTreeFS embed.FS
// EmbeddedDefaultsBytes returns the raw embedded defaults YAML. Surface: the // EmbeddedDefaultsZip packages the embedded per-depth default policy tree as a
// show-defaults CLI dumps these to stdout. // .zddc.zip with member paths using the "*" wildcard. The show-defaults CLI
func EmbeddedDefaultsBytes() []byte { // emits this so an operator can drop it at <ZDDC_ROOT>/.zddc.zip — or any
out := make([]byte, len(defaultsBytes)) // directory — and edit, add, or delete individual members. Mounting it
copy(out, defaultsBytes) // (optionally with inherit:false + acl.inherit:false to fully replace the
return out // baseline) is how a deployment customizes the shipped policy.
func EmbeddedDefaultsZip() ([]byte, error) {
var buf bytes.Buffer
zw := zip.NewWriter(&buf)
err := fs.WalkDir(defaultsTreeFS, "defaults", func(p string, d fs.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
if d.IsDir() {
return nil
}
member := strings.ReplaceAll(strings.TrimPrefix(p, "defaults/"), AnyPlaceholder+"/", "*/")
data, err := fs.ReadFile(defaultsTreeFS, p)
if err != nil {
return err
}
w, err := zw.Create(member)
if err != nil {
return err
}
_, err = w.Write(data)
return err
})
if err != nil {
return nil, err
}
if err := zw.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
} }
var ( var (
@ -35,8 +62,7 @@ var (
) )
// EmbeddedPolicyTree returns the baked-in per-depth default policy tree, // EmbeddedPolicyTree returns the baked-in per-depth default policy tree,
// memoised. This is the embedded form of the .zddc.zip mounted at the // memoised. The embedded form of the .zddc.zip mounted at the deployment root.
// deployment root (the bottom of every cascade).
func EmbeddedPolicyTree() (PolicyTree, error) { func EmbeddedPolicyTree() (PolicyTree, error) {
embeddedTreeOnce.Do(func() { embeddedTreeOnce.Do(func() {
embeddedTree, embeddedTreeErr = LoadPolicyTreeFromFS(defaultsTreeFS, "defaults") embeddedTree, embeddedTreeErr = LoadPolicyTreeFromFS(defaultsTreeFS, "defaults")
@ -54,7 +80,7 @@ var (
// tree into the single nested ZddcFile the cascade walker consumes, memoised. // tree into the single nested ZddcFile the cascade walker consumes, memoised.
// //
// The cascade walker (EffectivePolicy) consults this as the bottom-most level // The cascade walker (EffectivePolicy) consults this as the bottom-most level
// unless an on-disk .zddc up the chain sets `inherit: false`. // unless an on-disk .zddc / .zddc.zip up the chain sets `inherit: false`.
func EmbeddedDefaults() (ZddcFile, error) { func EmbeddedDefaults() (ZddcFile, error) {
embeddedDefaultsOnce.Do(func() { embeddedDefaultsOnce.Do(func() {
tree, err := EmbeddedPolicyTree() tree, err := EmbeddedPolicyTree()

View file

@ -1,319 +0,0 @@
# 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 ─────────────────────────────────────────────────────────
#
# Three roles ship empty (no members) — a fresh deployment grants
# nothing until an operator populates them. Membership UNIONS across
# the cascade; use `reset: true` at a subtree to start fresh.
#
# document_controller — owns the committed record and the party
# registry. They:
# - register parties: a party EXISTS iff ssr/<party>.yaml exists,
# and the DC creates it (rwc at ssr/). This is the single
# source of truth for party existence.
# - file write-once into the WORM archive: read + create at
# archive/<party>/received and issued via the worm: list (the
# WORM mask strips w/d/a; create survives only for listed
# principals). archive/ also grants rwc so the DC can create
# party record dirs.
# - rwcda across the live workspaces (incoming/working/staging/
# reviewing), restated per-peer so a DC matched by the
# project_team wildcard keeps full authority via within-level
# union.
# NOT a subtree-admin anywhere — no admins: entry. DCs cannot
# bypass WORM (only worm-create); admin elevation is reserved for
# the root admins: list (the human escape hatch for mis-filed
# documents or recovery).
#
# project_team — everyone working on a project. Read across the
# project, with a one-way ratchet through the live workspaces:
# working/ cr create + read; auto_own gives the creator
# rwcda inside the party folder they make
# staging/ cr drop + read, no modify after the drop
# reviewing/ cr create + read review iterations
# incoming/ r counterparty's drop zone (observe)
# archive/ r the committed record (received/issued), WORM
# ssr/mdl/rsk r registry + registers (the DC maintains them)
# Each handoff drops the role's modify rights for the previous
# stage.
#
# observer — pure read-only across the project; no create anywhere.
# Intended for auditors, regulators, and external read-only
# viewers who must not contribute content.
roles:
document_controller:
members: []
project_team:
members: []
observer:
members: []
# Universal tool baseline. archive (record browser), browse (file
# tree, hosts the in-place markdown editor), and landing (project
# picker) work everywhere. Each peer below adds its own tools
# (transmittal in staging/, tables in mdl/rsk/ssr, etc.). available_tools
# UNIONS across the cascade — 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:
#
# <dir>/ (trailing slash) → `dir_tool` — the directory view
# (defaults to `browse`, the file-tree
# navigator; you rarely set it).
# <dir> (no slash) → `default_tool` — the specialized app
# for this folder (archive, transmittal,
# tables). If a folder declares no
# default_tool, the no-slash form 302s
# to the slash form.
#
# JSON listing requests are unaffected — they always get the raw
# directory listing, so the browse SPA (and any client) can enumerate
# entries regardless of dir_tool/default_tool. Both keys cascade
# leaf→root.
#
# ── Canonical project structure (top-level party peers) ─────────────────────
#
# A project is a top-level directory. Under it sit a FLAT set of
# physical, party-partitioned peers — there are no virtual aggregators:
#
# archive/<party>/{received,issued}/ the committed record. PURE
# WORM (one rule on archive/, no
# exceptions): write/delete
# stripped for all; create only
# for document_controller (the
# worm: list); admins bypass.
# Party record dirs appear on the
# first filing.
# incoming/<party>/ counterparty drop zone
# reviewing/<party>/<tracking>/ we review their submission
# working/<party>/ our drafts (edit-history on)
# staging/<party>/<tracking>/ assemble transmittals
# mdl/<party>/*.yaml master document list (tables)
# rsk/<party>/*.yaml risk register (tables)
# ssr/<party>.yaml submittal status register — AND
# the AUTHORITATIVE PARTY REGISTRY
#
# Party registry: `ssr/<party>.yaml` existence is the SINGLE source of
# truth for "party <party> exists". Creating it (rwc at ssr/, via the
# SSR form) is how a party is born. Every OTHER peer carries
# `party_source: ssr`, so you cannot create <peer>/<party>/… — archive
# filing included — until the ssr row exists; the server 409s otherwise.
# ssr/ itself has no party_source (it is the source).
#
# mdl/ and rsk/ AGGREGATE: the peer root renders ALL parties in one
# table (a $party column derived from the real subdir), <peer>/<party>/
# shows that party's rows. ssr/ aggregates naturally (one flat file per
# party). $party is a real directory level, not a synthesized column.
#
# Mkdir at the project root is restricted to the peer names above plus
# system (_/.-prefixed) names (see handler/fileapi.go). Nothing here
# needs to exist on disk — the cascade resolves behaviour so a fresh
# project lands on usable empty views at every well-known URL. Operators
# override by mirroring this structure in an on-disk .zddc.
paths:
# First segment under root is the project name; "*" matches any.
"*":
# Project-scoped baseline ACL. project_team and observer read across
# the project; document_controller gets read + overwrite-existing.
# None gets `c` here — create is granted only at the specific peers
# below (archive/, ssr/, and the workspaces).
acl:
permissions:
project_team: r
observer: r
document_controller: rw
paths:
# ── The committed record: pure WORM ─────────────────────────
archive:
default_tool: archive
# A record can only be filed for a registered party.
party_source: ssr
# The ONE WORM rule. Cascades to <party>/{received,issued}:
# write/delete stripped for everyone; create survives only for
# document_controller; admins bypass (the escape hatch).
worm: [document_controller]
# rwc so a DC can create party record dirs (WORM masks w/d to
# leave read + write-once-create).
acl:
permissions:
document_controller: rwc
# ── Authoritative party registry + submittal status register ─
ssr:
default_tool: tables
available_tools: [tables]
# NO party_source — ssr/ IS the source of party existence.
# rwc: a DC registers a party by creating ssr/<party>.yaml and
# maintains its status (overwrite). Delete (de-register) is left
# to admins so a party with archived records is never orphaned.
acl:
permissions:
document_controller: rwc
history: true
records:
"*.yaml":
field_defaults:
kind: SSR
locked: [kind]
# ── Inbound workspace: counterparty drop zone ───────────────
incoming:
default_tool: classifier
available_tools: [classifier]
party_source: ssr
# The other party's DC uploads here (a deployment grants them
# cr, e.g. acl: { permissions: { "*@acme.com": cr } } at
# incoming/Acme/.zddc); OUR DC QCs via classifier and moves to
# archive/<party>/received. project_team has read only (observe).
acl:
permissions:
document_controller: rwcd
paths:
"*": # incoming/<party>
auto_own: true
drop_target: true
# ── Inbound workspace: review of their submission ───────────
reviewing:
default_tool: browse
available_tools: [browse]
party_source: ssr
# The Plan-Review composite endpoint scaffolds a folder here per
# submittal under review, with a .zddc carrying received_path
# back to the canonical record in archive/<party>/received.
acl:
permissions:
project_team: cr
document_controller: rwcda
paths:
"*": # reviewing/<party>
auto_own: true
drop_target: true
# ── Outbound workspace: our drafts (edit-history on) ────────
working:
default_tool: browse
available_tools: [browse, classifier]
party_source: ssr
# Subtree-inheriting: every markdown save under working/ is
# snapshotted to .zddc.d/history/<stem>/ with a server-stamped
# audit line. Reads of recorded history never require this flag.
history: true
acl:
permissions:
project_team: cr
document_controller: rwcda
paths:
"*": # working/<party> — auto-owned by its creator
auto_own: true
drop_target: true
# ── Outbound workspace: assemble transmittals ───────────────
staging:
default_tool: transmittal
available_tools: [transmittal, classifier]
party_source: ssr
# project_team drops files (cr); after the drop the doc-control
# workflow owns it. DC gets rwcda — `d` for the cut to issued/,
# `a` so Plan Review can write staging/<tracking>/.zddc.
acl:
permissions:
project_team: cr
document_controller: rwcda
paths:
"*": # staging/<party>
auto_own: true
drop_target: true
# ── Master document list (aggregates across parties) ────────
mdl:
default_tool: tables # peer root: all-parties table
available_tools: [tables]
party_source: ssr
history: true
# The deliverables register is collaboratively editable: the DC
# manages it (rwcd) and project_team can create + edit rows (rwc,
# no delete) — every change is captured by the history: audit above,
# so broad write is safe. This project_team: rwc overrides the
# project-level project_team: r (deepest matching level wins).
acl:
permissions:
document_controller: rwcd
project_team: rwc
# field_codes: constrain tracking-number components here (or
# higher in the cascade). Three kinds — enum / pattern / free;
# map-merge across levels. originator is folder-bound (below),
# so it is not listed here. Example:
# field_codes:
# discipline: { kind: enum, codes: { EL: Electrical, ME: Mechanical } }
# sequence: { kind: pattern, pattern: "[0-9]{4}" }
paths:
"*": # mdl/<party>: that party's rows, flat
default_tool: tables
# MDL records: each .yaml is an independent deliverable with
# its own composed tracking number. originator is the party
# folder (the record's own dir, distance 0 above
# mdl/<party>/<file>.yaml) and renders read-only — the folder
# is the single source of truth for the originator code.
#
# To add project-wide components (phase, area, …), override
# filename_format here AND mdl/<party>/{form,table}.yaml.
records:
"*.yaml":
folder_fields:
originator: 0
filename_format: "{originator}-{project}-{discipline}-{type}-{sequence}-{suffix?}"
# ── Risk register (aggregates across parties) ───────────────
rsk:
default_tool: tables
available_tools: [tables]
party_source: ssr
history: true
# Same as mdl/: DC manages (rwcd), project_team creates + edits rows
# (rwc, no delete); the history: audit covers every change.
acl:
permissions:
document_controller: rwcd
project_team: rwc
paths:
"*": # rsk/<party>
default_tool: tables
# RSK records: each .yaml is a row of a parent rsk-type
# deliverable; the server auto-assigns -{row} within the
# row-scope group on POST-create. originator is folder-bound
# to the party folder, same as MDL.
records:
"*.yaml":
folder_fields:
originator: 0
filename_format: "{originator}-{project}-{discipline}-{type}-{sequence}-{suffix?}-{row}"
field_defaults:
type: RSK
locked: [type]
row_field: row
row_scope_fields: [originator, project, discipline, type, sequence, suffix]

View file

@ -1,15 +1,16 @@
package zddc package zddc
import ( import (
"archive/zip"
"bytes"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"testing" "testing"
) )
// TestEmbeddedDefaultsParse — the shipped defaults.zddc.yaml must // TestEmbeddedDefaultsParse — the embedded per-depth default tree must assemble
// parse cleanly into a ZddcFile. Regression guard against accidental // + parse cleanly into a ZddcFile. Regression guard against a broken member.
// YAML syntax errors in the source-of-truth file.
func TestEmbeddedDefaultsParse(t *testing.T) { func TestEmbeddedDefaultsParse(t *testing.T) {
zf, err := EmbeddedDefaults() zf, err := EmbeddedDefaults()
if err != nil { if err != nil {
@ -20,16 +21,35 @@ func TestEmbeddedDefaultsParse(t *testing.T) {
} }
} }
// TestEmbeddedDefaultsBytesDumpable — the bytes used by the show- // TestEmbeddedDefaultsZipDumpable — the .zddc.zip emitted by show-defaults must
// defaults CLI must be non-empty and start with a comment so an // be a valid archive carrying the per-depth policy members with "*" wildcard
// operator pasting them into a real file sees the header. // segments (no leftover _any_ placeholder).
func TestEmbeddedDefaultsBytesDumpable(t *testing.T) { func TestEmbeddedDefaultsZipDumpable(t *testing.T) {
got := EmbeddedDefaultsBytes() b, err := EmbeddedDefaultsZip()
if len(got) == 0 { if err != nil {
t.Fatal("EmbeddedDefaultsBytes returned empty slice") t.Fatalf("EmbeddedDefaultsZip: %v", err)
} }
if !strings.HasPrefix(strings.TrimLeft(string(got), " \t"), "#") { zr, err := zip.NewReader(bytes.NewReader(b), int64(len(b)))
t.Errorf("expected leading comment, got: %q", string(got[:60])) if err != nil {
t.Fatalf("not a valid zip: %v", err)
}
var hasRoot, hasWildcard bool
for _, f := range zr.File {
if strings.Contains(f.Name, AnyPlaceholder) {
t.Errorf("member %q still has the _any_ placeholder; want * wildcard", f.Name)
}
switch f.Name {
case ".zddc":
hasRoot = true
case "*/working/.zddc":
hasWildcard = true
}
}
if !hasRoot {
t.Error("zip missing root .zddc member")
}
if !hasWildcard {
t.Error(`zip missing "*/working/.zddc" member`)
} }
} }

View file

@ -200,7 +200,7 @@ func EnsureCanonicalAncestors(fsRoot, target, principalEmail string, perm fs.Fil
// Determine if this newly-created ancestor is an auto-own // Determine if this newly-created ancestor is an auto-own
// position and whether it should be fenced (inherit: false). // position and whether it should be fenced (inherit: false).
// Resolved via the .zddc cascade — defaults.zddc.yaml // Resolved via the .zddc cascade — internal/zddc/defaults/
// carries the canonical "working/staging auto-own + per-user // carries the canonical "working/staging auto-own + per-user
// homes fenced + incoming auto-own" convention, and any // homes fenced + incoming auto-own" convention, and any
// on-disk .zddc can override per-directory. // on-disk .zddc can override per-directory.

View file

@ -347,7 +347,7 @@ func ChildrenDeclaredAt(fsRoot, dirPath string) []string {
// (received, issued) — or "" if the path is not at a canonical slot. // (received, issued) — or "" if the path is not at a canonical slot.
// //
// Detection is structural against the flat-peer layout declared in // Detection is structural against the flat-peer layout declared in
// defaults.zddc.yaml: // internal/zddc/defaults/:
// //
// - second-level <project>/<peer> for any top-level peer. // - second-level <project>/<peer> for any top-level peer.
// - third-level <project>/<peer>/<party> reports its peer (slot) for // - third-level <project>/<peer>/<party> reports its peer (slot) for

View file

@ -7,7 +7,7 @@ import (
) )
// TestDefaultToolAt_FromEmbeddedConvention — the canonical default-tool // TestDefaultToolAt_FromEmbeddedConvention — the canonical default-tool
// rules in defaults.zddc.yaml resolve correctly for the flat top-level // rules in internal/zddc/defaults/ resolve correctly for the flat top-level
// peers (and their per-party subdirs) without any on-disk .zddc. // peers (and their per-party subdirs) without any on-disk .zddc.
func TestDefaultToolAt_FromEmbeddedConvention(t *testing.T) { func TestDefaultToolAt_FromEmbeddedConvention(t *testing.T) {
resetCache() resetCache()

View file

@ -185,7 +185,7 @@ func lookupRoleMembers(chain PolicyChain, levelIdx int, roleName string) ([]stri
} }
// The embedded defaults sit below chain.Levels[0] in the cascade. // The embedded defaults sit below chain.Levels[0] in the cascade.
// Fold its role definitions in as the baseline (so a role declared // Fold its role definitions in as the baseline (so a role declared
// only in defaults.zddc.yaml is "defined", and a deployment's // only in internal/zddc/defaults/ is "defined", and a deployment's
// on-disk redefinition unions on top). Skipped above only if a // on-disk redefinition unions on top). Skipped above only if a
// reset:true level already returned. // reset:true level already returned.
if role, ok := chain.Embedded.Roles[roleName]; ok { if role, ok := chain.Embedded.Roles[roleName]; ok {

View file

@ -14,7 +14,7 @@ import "strings"
// //
// Note the layering: the names are hard-coded here, but per-peer // Note the layering: the names are hard-coded here, but per-peer
// BEHAVIOR (default_tool, worm, party_source, history, auto_own, …) // BEHAVIOR (default_tool, worm, party_source, history, auto_own, …)
// stays cascade-driven in defaults.zddc.yaml + on-disk .zddc. This file // stays cascade-driven in internal/zddc/defaults/ + on-disk .zddc. This file
// is identity/shape only. // is identity/shape only.
var ( var (
// projectPeers: the flat set of physical directories permitted // projectPeers: the flat set of physical directories permitted

View file

@ -6,7 +6,7 @@ import (
) )
// The .zddc cascade is the authority for canonical-folder behaviour; // The .zddc cascade is the authority for canonical-folder behaviour;
// see defaults.zddc.yaml for the conventions and lookups.go for the // see internal/zddc/defaults/ for the conventions and lookups.go for the
// helpers consumers call (DefaultToolAt, AutoOwnAt, VirtualAt, // helpers consumers call (DefaultToolAt, AutoOwnAt, VirtualAt,
// IsDeclaredPath, ChildrenDeclaredAt, AvailableToolsAt). // IsDeclaredPath, ChildrenDeclaredAt, AvailableToolsAt).

View file

@ -10,7 +10,7 @@ import (
// TestWormMaskStripsWDA retired in the cascade-config migration. The // TestWormMaskStripsWDA retired in the cascade-config migration. The
// `auto_own:` and `worm:` .zddc keys carry these conventions now — // `auto_own:` and `worm:` .zddc keys carry these conventions now —
// see lookups_test.go (AutoOwnAt) and worm_test.go (WormZoneGrant); // see lookups_test.go (AutoOwnAt) and worm_test.go (WormZoneGrant);
// the canonical defaults live in defaults.zddc.yaml. // the canonical defaults live in internal/zddc/defaults/.
func TestResolveCanonicalCaseFold(t *testing.T) { func TestResolveCanonicalCaseFold(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
@ -56,6 +56,6 @@ func TestResolveCanonicalMissingParent(t *testing.T) {
} }
// TestCanonicalLists and TestIsProjectRootFolder retired in Phase 3 — // TestCanonicalLists and TestIsProjectRootFolder retired in Phase 3 —
// the canonical convention is now expressed in defaults.zddc.yaml and // the canonical convention is now expressed in internal/zddc/defaults/ and
// asserted by lookups_test.go (TestDefaultToolAt_FromEmbeddedConvention, // asserted by lookups_test.go (TestDefaultToolAt_FromEmbeddedConvention,
// TestIsDeclaredPath_FromEmbeddedConvention, etc.). // TestIsDeclaredPath_FromEmbeddedConvention, etc.).

View file

@ -26,7 +26,7 @@ func TestStandardRoles_DocControllerScopedCreate(t *testing.T) {
resetCache() resetCache()
root := t.TempDir() root := t.TempDir()
// DC authority comes PURELY from the cascade peer grants in // DC authority comes PURELY from the cascade peer grants in
// defaults.zddc.yaml — no auto-own / admins: list. DCs are typically // internal/zddc/defaults/ — no auto-own / admins: list. DCs are typically
// in project_team too (the *@example.com wildcard); the defaults // in project_team too (the *@example.com wildcard); the defaults
// restate document_controller at each peer so the within-level union // restate document_controller at each peer so the within-level union
// gives the DC the higher grant. // gives the DC the higher grant.
@ -221,7 +221,7 @@ func TestStandardRoles_ProjectTeamInFlightRatchet(t *testing.T) {
// TestStandardRoles_DocControllerStagingDelete — DC needs `d` at // TestStandardRoles_DocControllerStagingDelete — DC needs `d` at
// staging/ to perform the staging-to-issued transfer (cut, not copy); // staging/ to perform the staging-to-issued transfer (cut, not copy);
// the explicit document_controller: rwcd grant supplies it. Mirrors // the explicit document_controller: rwcd grant supplies it. Mirrors
// the incoming/ pattern (line 286-288 of defaults.zddc.yaml) where // the incoming/ pattern (line 286-288 of internal/zddc/defaults/) where
// the QC + transfer-out workflow needed the same. // the QC + transfer-out workflow needed the same.
func TestStandardRoles_DocControllerStagingDelete(t *testing.T) { func TestStandardRoles_DocControllerStagingDelete(t *testing.T) {
resetCache() resetCache()

View file

@ -6,7 +6,7 @@ package zddc
// //
// Replaces the hardcoded IsWormPath / WormFolderLevelIndex / WormMask // Replaces the hardcoded IsWormPath / WormFolderLevelIndex / WormMask
// machinery (which keyed off the literal folder names "received" and // machinery (which keyed off the literal folder names "received" and
// "issued"). The convention now lives in defaults.zddc.yaml — those // "issued"). The convention now lives in internal/zddc/defaults/ — those
// two folders carry `worm: {}` — and any operator can mark another // two folders carry `worm: {}` — and any operator can mark another
// directory WORM by adding `worm:` to its .zddc. // directory WORM by adding `worm:` to its .zddc.

View file

@ -6,7 +6,7 @@ import (
"testing" "testing"
) )
// TestWormZoneGrant_EmbeddedConvention — defaults.zddc.yaml declares // TestWormZoneGrant_EmbeddedConvention — internal/zddc/defaults/ declares
// `worm: [document_controller]` on archive/, so the ENTIRE archive // `worm: [document_controller]` on archive/, so the ENTIRE archive
// subtree is a WORM zone (inWorm=true). With no role members in this // subtree is a WORM zone (inWorm=true). With no role members in this
// bare fixture the grant for an arbitrary principal is 0. The top-level // bare fixture the grant for an arbitrary principal is 0. The top-level

View file

@ -119,24 +119,6 @@ func TestPolicyTreeAlong(t *testing.T) {
} }
} }
// Phase-4 gate: the embedded per-depth tree assembles to EXACTLY the legacy
// defaults.zddc.yaml ZddcFile, so pointing EmbeddedDefaults at the tree is a
// behavioral no-op. (The Layer-2 matrix is the decision-level confirmation.)
func TestEmbeddedTreeMatchesYAML(t *testing.T) {
tree, err := EmbeddedPolicyTree()
if err != nil {
t.Fatalf("embedded tree: %v", err)
}
assembled := tree.Assemble()
legacy, err := parseBytes(defaultsBytes)
if err != nil {
t.Fatalf("parse legacy yaml: %v", err)
}
if !reflect.DeepEqual(assembled, legacy) {
t.Errorf("assembled per-depth tree != legacy defaults.zddc.yaml\n assembled=%+v\n legacy=%+v", assembled, legacy)
}
}
func keysOf(t PolicyTree) []string { func keysOf(t PolicyTree) []string {
out := make([]string, 0, len(t)) out := make([]string, 0, len(t))
for k := range t { for k := range t {