docs: post-reshape + role-redesign refresh
All checks were successful
Deploy content to live site / deploy (push) Successful in 2s

Catches the website up to the v0.0.21 server contract:

  - Project structure (reference.html §9): archive/ is the only
    physical project-root directory; the in-flight lifecycle
    (working/staging/reviewing) now lives PER-PARTY under
    archive/<party>/. Six top-level URLs (ssr/mdl/rsk/working/
    staging/reviewing) are virtual aggregators synthesised from
    each party's content.
  - Retired the staging↔working mirror language — drafting a
    response transmittal now walks the in-flight ratchet through
    Plan Review's scaffold at archive/<party>/reviewing/<tracking>/.
  - Role descriptions (§10): document_controller is no longer
    subtree-admin anywhere. Authority cascades from the auto-own
    .zddc written at each archive/<party>/ folder, which grants
    both the creator email AND the document_controller role
    `rwcda` (via auto_own_roles in the defaults). Multi-DC
    deployments work without admin status because the role itself
    is named in every party's auto-own grant.
  - Added the `observer` role (third standard role) with a
    pure-read-only intent for external auditors.
  - Documented the in-flight ratchet (working → staging → issued)
    as a one-way handoff that downgrades the prior role's modify
    rights at each step.
  - Clarified that the `a` verb is the .zddc-edit verb, distinct
    from the elevation-bypass sudo channel (root admins: list).
  - Dropped `on_plan_review:` from the cascade-keys reference (the
    key was retired when Plan Review hardcoded the scaffold
    convention); added `auto_own_roles:` and `auto_own_fenced:`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZDDC 2026-05-21 11:27:26 -05:00
parent 06916e5884
commit 38c8b0cfa1
2 changed files with 78 additions and 51 deletions

View file

@ -134,7 +134,7 @@
<p style="margin-top: var(--spacing-md);"><strong><code class="inline">zddc-server</code></strong> is a small Go binary purpose-built to serve ZDDC archives. <em>Any</em> web server gives you online mode; <code class="inline">zddc-server</code> adds things a generic web server can't:</p> <p style="margin-top: var(--spacing-md);"><strong><code class="inline">zddc-server</code></strong> is a small Go binary purpose-built to serve ZDDC archives. <em>Any</em> web server gives you online mode; <code class="inline">zddc-server</code> adds things a generic web server can't:</p>
<ul class="feature-list"> <ul class="feature-list">
<li><strong>Lazy folder creation, case-fold matching.</strong> Drop a <code class="inline">.zddc</code> file into an empty directory and the canonical project layout (<code class="inline">working/</code>, <code class="inline">staging/</code>, <code class="inline">archive/&lt;party&gt;/{mdl,incoming,received,issued}/</code>) materialises on the first write into each path — never on bare reads. Folder names are matched case-insensitively, so an existing <code class="inline">Working/</code> is reused rather than shadowed by a new <code class="inline">working/</code> sibling. Each authenticated viewer sees a virtual <code class="inline">working/&lt;your-email&gt;/</code> entry; first write makes it real.</li> <li><strong>Lazy folder creation, case-fold matching.</strong> Drop a <code class="inline">.zddc</code> file into an empty directory and the canonical project layout (<code class="inline">archive/&lt;party&gt;/{mdl,rsk,incoming,received,issued,working,staging,reviewing}/</code>) materialises on the first write into each path — never on bare reads. <code class="inline">archive/</code> is the only physical project-root child; <code class="inline">ssr</code>, <code class="inline">mdl</code>, <code class="inline">rsk</code>, <code class="inline">working</code>, <code class="inline">staging</code>, <code class="inline">reviewing</code> sit beside it as virtual aggregators that synthesise listings across parties. Folder names are matched case-insensitively, so an existing <code class="inline">Working/</code> is reused rather than shadowed by a new <code class="inline">working/</code> sibling. Each authenticated viewer sees a virtual <code class="inline">archive/&lt;party&gt;/working/&lt;your-email&gt;/</code> entry; first write makes it real.</li>
<li><strong>Virtual <code class="inline">.archive</code> URL space.</strong> <code class="inline">GET /Project/.archive/123-XYZ.html</code> resolves to the canonical revision file at request time. Computed from filenames; no cache, no separate index file.</li> <li><strong>Virtual <code class="inline">.archive</code> URL space.</strong> <code class="inline">GET /Project/.archive/123-XYZ.html</code> resolves to the canonical revision file at request time. Computed from filenames; no cache, no separate index file.</li>
<li><strong>Access control via <code class="inline">.zddc</code> files.</strong> Behind a reverse proxy that authenticates users and sets an <code class="inline">X-Auth-Request-Email</code> request header, <code class="inline">zddc-server</code> consults YAML <code class="inline">.zddc</code> files at every directory along the path. The cascade walks root→leaf; the closest match wins. Five verbs (<code class="inline">r</code> read, <code class="inline">w</code> overwrite, <code class="inline">c</code> create, <code class="inline">d</code> delete, <code class="inline">a</code> admin / edit ACL) gate every operation. An empty grant (e.g. <code class="inline">"*@vendor.com": ""</code>) is an explicit deny. A subtree that wants to start fresh — vendor folder, regulated workspace — can declare <code class="inline">inherit: false</code> to fence off ancestor grants and roles, then list the principals it does want. Common shapes (paired open/closed projects, third-party-restricted vendor folders) are documented with worked examples in the <a href="https://codeberg.org/VARASYS/ZDDC/src/branch/main/zddc/README.md#access-control-the-zddc-cascade">access-control reference</a>. No database, no admin UI.</li> <li><strong>Access control via <code class="inline">.zddc</code> files.</strong> Behind a reverse proxy that authenticates users and sets an <code class="inline">X-Auth-Request-Email</code> request header, <code class="inline">zddc-server</code> consults YAML <code class="inline">.zddc</code> files at every directory along the path. The cascade walks root→leaf; the closest match wins. Five verbs (<code class="inline">r</code> read, <code class="inline">w</code> overwrite, <code class="inline">c</code> create, <code class="inline">d</code> delete, <code class="inline">a</code> admin / edit ACL) gate every operation. An empty grant (e.g. <code class="inline">"*@vendor.com": ""</code>) is an explicit deny. A subtree that wants to start fresh — vendor folder, regulated workspace — can declare <code class="inline">inherit: false</code> to fence off ancestor grants and roles, then list the principals it does want. Common shapes (paired open/closed projects, third-party-restricted vendor folders) are documented with worked examples in the <a href="https://codeberg.org/VARASYS/ZDDC/src/branch/main/zddc/README.md#access-control-the-zddc-cascade">access-control reference</a>. No database, no admin UI.</li>
<li><strong>Roles for human-readable grants.</strong> A <code class="inline">.zddc</code> may declare named roles whose members are email patterns; permissions then reference the role name instead of pasting the same wildcard everywhere: <li><strong>Roles for human-readable grants.</strong> A <code class="inline">.zddc</code> may declare named roles whose members are email patterns; permissions then reference the role name instead of pasting the same wildcard everywhere:

View file

@ -1001,47 +1001,69 @@ date = 4DIGIT "-" 2DIGIT "-" 2DIGIT
project/ project/
.zddc ← the only file an operator must create .zddc ← the only file an operator must create
working/ ← Where staff draft. Each person has their own archive/ ← The only PHYSICAL project-root directory.
subfolder here (named by email) and works on Everything party-scoped (records, lifecycle,
documents in private until they're ready to immutable archives) lives uniformly under
issue. The browse tool handles everything here archive/&lt;party&gt;/.
— file management, in-place markdown editing
with live preview, on-demand format conversion.
staging/ ← The "about to issue" lane. A folder in here
declares a planned outbound transmittal: its
name carries the planned issue date and tracking
number, so anyone can see what's queued up and
when. Files in staging are the finalised set;
drafting still happens in working/.
reviewing/ ← One place to see everything we owe a response
on. Each row pairs an inbound submittal we've
accepted with our in-progress response draft —
saves staff from hunting across per-party
folders to find what's open.
archive/ ← The permanent record. Everything we've ever
formally exchanged with a counterparty lives
here, organised one folder per party.
{party-name}/ ← One per counterparty; one for ourselves. {party-name}/ ← One per counterparty; one for ourselves.
We treat our own organisation as just another We treat our own organisation as just another
party so internal deliverables get the same party so internal deliverables get the same
tracking treatment as external ones. tracking treatment as external ones.
mdl/ ← The party's Master Deliverables List — mdl/ ← The party's Master Deliverables List —
what they're going to produce, planned and what they're going to produce. Opens as
in-flight. Opens as a grid editor; rows are a grid editor; rows are individual .yaml
individual yaml files, one per deliverable. files, one per deliverable.
incoming/ ← Where that party drops things for us. Acts rsk/ ← Risk register for this party. Same grid
as a quality-check buffer before content editor as mdl/.
incoming/ ← Where the counterparty drops things for us.
A quality-check buffer before content
becomes part of the permanent record. becomes part of the permanent record.
received/ ← What we've accepted from that party. Immutable; received/ ← What we've accepted from that party.
the historical record of inbound documents. Immutable (WORM); the historical record
issued/ ← What we sent to that party. Immutable; the of inbound documents.
historical record of outbound documents. issued/ ← What we sent to that party. Immutable
(WORM); the historical record of outbound
documents.
working/ ← Where this party's staff draft. Each
person has a private subfolder here
(named by email) and iterates until ready
to commit. The browse tool handles
everything here.
&lt;your-email&gt;/ ← Your private workspace under THIS
party. Fenced (inherit:false) by the
auto-own .zddc, so other team members
only see what you explicitly share.
staging/ ← The "about to issue" lane for this party.
Drop files here and the project_team
gives them up — only the doc-controller
can change a file after it lands. Each
sub-folder declares a planned outbound
transmittal (name carries the date +
tracking number).
reviewing/ ← One place per party to see everything we
owe a response on. Folders are scaffolded
by Plan Review, each pairing an inbound
submittal in received/ with the in-progress
response draft.
ssr/ ← Virtual aggregator: tables rollup of every
party's ssr.yaml row, with a synthesised
$party column. Never on disk.
mdl/ ← Virtual aggregator: tables rollup of every
party's mdl/*.yaml deliverables.
rsk/ ← Virtual aggregator: tables rollup of every
party's rsk/*.yaml risk rows.
working/ ← Virtual aggregator: browse folder-nav listing
parties with non-empty content in working/.
Per-party clicks 302-redirect to the canonical
archive/&lt;party&gt;/working/.
staging/ ← Same shape as working/ for the staging slot.
reviewing/ ← Same shape as working/ for the reviewing slot.
</div> </div>
<p style="margin-top: var(--spacing-md); font-size: 0.95em; color: var(--color-text-soft);"><em>Mechanics:</em> folders materialise on first write, names match case-insensitively, the staging↔working pairing is automatic, the immutable folders enforce write-once via an ACL mask, and a virtual <em>&lt;your-email&gt;/</em> entry under <code>working/</code> creates your personal subfolder on first save. None of that needs to be in your head when you're using the system — drop files where the lifecycle says they go and the layout takes care of itself.</p> <p style="margin-top: var(--spacing-md); font-size: 0.95em; color: var(--color-text-soft);"><em>Mechanics:</em> folders materialise on first write, names match case-insensitively, the WORM zones (<code>received/</code>, <code>issued/</code>) enforce write-once via an ACL mask, and the six top-level aggregators (<code>ssr</code>/<code>mdl</code>/<code>rsk</code>/<code>working</code>/<code>staging</code>/<code>reviewing</code>) are virtual — they never materialise on disk but show up in listings, computed from <code>archive/&lt;party&gt;/&hellip;</code> at request time. Mkdir at the project root is restricted to <code>archive</code> + system names (<code>_</code>/<code>.</code>-prefixed) so the virtual names can never be shadowed by a physical folder. Drop files where the lifecycle says they go and the layout takes care of itself.</p>
<p style="margin-top: var(--spacing-md); font-size: 0.95em; color: var(--color-text-soft);"><em>The in-flight ratchet.</em> <code>working/</code><code>staging/</code><code>issued/</code> is a one-way handoff. <code>project_team</code> iterates freely in <code>working/</code> (the auto-own-fenced subfolder gives each user a private <code>rwcda</code> workspace). When they drop a file into <code>staging/</code> their access downgrades to <code>cr</code> — they can drop more, but only the <code>document_controller</code> can change what's already there. When DC publishes to <code>issued/</code>, the WORM mask downgrades even DC to <code>cr</code> (write-once). Each handoff is a commitment by permission-loss.</p>
<p><strong>5-step transmittal workflow:</strong></p> <p><strong>5-step transmittal workflow:</strong></p>
@ -1093,12 +1115,12 @@ project/
</div> </div>
<h3>Drafting a response transmittal</h3> <h3>Drafting a response transmittal</h3>
<p>Submittals from counterparties (tracking numbers containing <code>-SUB-</code> at status <code>IFR</code> or <code>IFA</code>) require a response transmittal whose status starts with <code>RS</code> (RSA, RSB, RSC, …). The drafting flow uses the same staging↔working pairing as a fresh outbound:</p> <p>Submittals from counterparties (tracking numbers containing <code>-SUB-</code> at status <code>IFR</code> or <code>IFA</code>) require a response transmittal whose status starts with <code>RS</code> (RSA, RSB, RSC, …). The flow walks the ratchet:</p>
<ol> <ol>
<li>Create <code>staging/&lt;YYYY-MM-DD&gt;_&lt;tracking&gt; (RSA) - &lt;title&gt;/</code>. The date is the planned issue (response-due) date.</li> <li>Right-click the submittal in <code>archive/&lt;party&gt;/received/&lt;tracking&gt;/</code> and pick <strong>Plan Review</strong>. The server scaffolds a workflow folder at <code>archive/&lt;party&gt;/reviewing/&lt;tracking&gt;/</code> — its <code>.zddc</code> carries a <code>received_path</code> pointer back to the canonical submittal and a planned response date.</li>
<li>The server mirrors the folder name into <code>working/</code>; reviewer notes and the response payload are drafted there.</li> <li>The party's working subfolder under <code>archive/&lt;party&gt;/working/&lt;your-email&gt;/</code> is where reviewer notes and the response payload are drafted. The <code>reviewing/</code> virtual aggregator at the project root surfaces all open reviews across parties.</li>
<li>The <code>reviewing/</code> virtual surface lists each in-progress response paired with the source submittal in <code>archive/&lt;party&gt;/received/</code>.</li> <li>When the response is ready, files move from <code>working/</code> into <code>archive/&lt;party&gt;/staging/</code> for sign-off. Project team's permission on staged files downgrades to <code>cr</code> — the doc controller takes over.</li>
<li>When the response is ready, files move from <code>working/</code> into <code>staging/</code> for sign-off, then through the standard 5-step transmittal flow into <code>archive/&lt;party&gt;/issued/</code>. Both the staging and working folders are deleted at issue time.</li> <li>The doc controller cuts the package from <code>staging/</code> into <code>archive/&lt;party&gt;/issued/</code> via the standard 5-step transmittal flow. The reviewing scaffold is deleted at issue time.</li>
</ol> </ol>
</section> </section>
@ -1126,7 +1148,7 @@ project/
<h3>Step 2 — per-project <code>&lt;project&gt;/.zddc</code></h3> <h3>Step 2 — per-project <code>&lt;project&gt;/.zddc</code></h3>
<p>In each project, populate the <code>document_controller</code> and <code>project_team</code> role members:</p> <p>In each project, populate role members:</p>
<div class="code-block">title: "Project Phoenix" <div class="code-block">title: "Project Phoenix"
roles: roles:
@ -1135,17 +1157,22 @@ roles:
- dc1@burnsmcd.com - dc1@burnsmcd.com
project_team: project_team:
members: members:
- alice@burnsmcd.com - '*@burnsmcd.com' # all internal staff
- '*@acme.com' # external counterparty (glob) observer: # optional — external audit
members:
- auditor@external.com
</div> </div>
<p>That's it. The embedded cascade does the rest:</p> <p>That's it. The embedded cascade does the rest:</p>
<ul style="margin: var(--spacing-md) 0; padding-left: 1.5rem; color: var(--color-text-muted); line-height: 1.65;"> <ul style="margin: var(--spacing-md) 0; padding-left: 1.5rem; color: var(--color-text-muted); line-height: 1.65;">
<li><code>project_team</code> gets read across the project</li> <li><code>project_team</code> gets read across the project plus the in-flight ratchet (<code>cr</code> in <code>working/</code> + <code>reviewing/</code> + <code>staging/</code>, with <code>rwcda</code> inside each user's auto-own-fenced home under <code>working/</code>).</li>
<li><code>document_controller</code> gets read+write project-wide, plus create authority on <code>archive/</code>, WORM filing rights on <code>received/</code> and <code>issued/</code>, and subtree-admin of <code>working/</code>, <code>staging/</code>, and <code>reviewing/</code></li> <li><code>document_controller</code> creates party folders at <code>archive/</code>; when they do, the auto-own <code>.zddc</code> written at <code>archive/&lt;party&gt;/</code> grants both their email AND the <code>document_controller</code> role <code>rwcda</code> — so any DC in the role has full authority at every party a peer created. Explicit <code>rwcd</code> grants at <code>incoming/</code>/<code>staging/</code> for the QC + transfer workflows, and WORM <code>cr</code> at <code>received/</code>/<code>issued/</code> for write-once filing.</li>
<li><code>observer</code> is pure read-only across the project — intended for external auditors, regulators, and read-only viewers who must not contribute content.</li>
</ul> </ul>
<p style="margin-top: var(--spacing-md);">A DC is typically also a project team member (the <code>*@burnsmcd.com</code> glob catches them). The embedded defaults restate <code>document_controller: rwcda</code> at every slot that grants project_team a narrower verb — within-level union of all matched principals gives DCs <code>rwcda</code> <code>cr</code> = <code>rwcda</code>, preserving full authority. <strong>Document controllers are not subtree-admins anywhere.</strong> Their power comes purely from cascade grants; admin elevation is reserved for the root <code>admins:</code> list (the human escape hatch).</p>
<p>Add a new project team member with one line; revoke by removing the line. No need to restate the cascade's grants — they're already in the embedded defaults that ship with <code>zddc-server</code>.</p> <p>Add a new project team member with one line; revoke by removing the line. No need to restate the cascade's grants — they're already in the embedded defaults that ship with <code>zddc-server</code>.</p>
<h3>Schema essentials</h3> <h3>Schema essentials</h3>
@ -1189,7 +1216,7 @@ roles:
<li><code>w</code> write (overwrite existing files)</li> <li><code>w</code> write (overwrite existing files)</li>
<li><code>c</code> create (new files, new directories)</li> <li><code>c</code> create (new files, new directories)</li>
<li><code>d</code> delete</li> <li><code>d</code> delete</li>
<li><code>a</code> admin (subtree-admin at this level and below)</li> <li><code>a</code> admin (modify the <code>.zddc</code> ACL at this level — distinct from the root <code>admins:</code> list, which is the elevation-bypass sudo channel)</li>
</ul> </ul>
<p>An empty bits string (<code>''</code>) is an explicit deny.</p> <p>An empty bits string (<code>''</code>) is an explicit deny.</p>
@ -1229,7 +1256,7 @@ request until you populate it..."</div>
<div class="code-block">zddc-server show-defaults</div> <div class="code-block">zddc-server show-defaults</div>
<p>That prints the embedded <code>defaults.zddc.yaml</code> with comments explaining every option (<code>worm:</code>, <code>auto_own:</code>, <code>drop_target:</code>, <code>apps:</code>, <code>convert:</code>, <code>on_plan_review:</code>, <code>records:</code>, <code>available_tools:</code>, <code>default_tool:</code>, <code>dir_tool:</code>, and more). Pipe it to a file and use it as the starting point for any deeper customization.</p> <p>That prints the embedded <code>defaults.zddc.yaml</code> with comments explaining every option (<code>worm:</code>, <code>auto_own:</code>, <code>auto_own_roles:</code>, <code>auto_own_fenced:</code>, <code>drop_target:</code>, <code>apps:</code>, <code>convert:</code>, <code>records:</code>, <code>available_tools:</code>, <code>default_tool:</code>, <code>dir_tool:</code>, and more). Pipe it to a file and use it as the starting point for any deeper customization.</p>
</section> </section>
<!-- Section 11: Tools --> <!-- Section 11: Tools -->