# 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]
