# 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/.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//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: # # / (trailing slash) → `dir_tool` — the directory view # (defaults to `browse`, the file-tree # navigator; you rarely set it). # (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//{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// counterparty drop zone # reviewing/// we review their submission # working// our drafts (edit-history on) # staging/// assemble transmittals # mdl//*.yaml master document list (tables) # rsk//*.yaml risk register (tables) # ssr/.yaml submittal status register — AND # the AUTHORITATIVE PARTY REGISTRY # # Party registry: `ssr/.yaml` existence is the SINGLE source of # truth for "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 //… — 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), // # 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 /{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/.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//received. project_team has read only (observe). acl: permissions: document_controller: rwcd paths: "*": # incoming/ 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//received. acl: permissions: project_team: cr document_controller: rwcda paths: "*": # reviewing/ 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// 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/ — 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//.zddc. acl: permissions: project_team: cr document_controller: rwcda paths: "*": # staging/ 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/: 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//.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//{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/ 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]