Commit graph

6 commits

Author SHA1 Message Date
662bfbdbf9 refactor(records): converge all record-write paths on WriteWithHistory
The in-dir form create/update (serveFormCreate/serveFormUpdate) wrote
records with plain WriteAtomic + date+email naming — no audit stamping,
no filename composition, no field_codes/folder_fields. So "+ Add row"
from a per-party mdl/rsk table produced un-stamped, mis-named rows that
the tables tool's own PUT-update path (which composes) would then 422
on. Only PUT and the project rollup honored the record machinery.

Now every record-write entry point converges on WriteWithHistory:

- Extract the shared field_defaults + folder_fields + row-assign +
  compose step into recordCreatePrep (history.go); the rollup uses it
  too, replacing its inline copy.
- serveFormCreate: when a records: rule with a filename_format applies
  in the target dir, compose the name + route through WriteWithHistory;
  otherwise keep the generic date+email submission write.
- serveFormUpdate: route through WriteWithHistory unconditionally — it
  stamps/historizes records and plain-writes non-records. Editing a
  tracking-number component in place now 422s (identity is the
  filename; renames are delete+create).
- Drop originator from required: in the per-party mdl/rsk forms and mark
  it readOnly, matching the rollup forms — it's server-derived from the
  party folder, so a create needn't send it.

Docs (AGENTS.md, ARCHITECTURE.md) updated for the converged wire
surface. Tests: in-dir record create composes + stamps audit +
folder-binds originator; in-dir update bumps revision and rejects an
in-place component edit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 14:48:52 -05:00
e3db2f8473 feat(records): simplest default tracking number + folder-bound originator
Two coupled cleanups so the baked-in defaults reflect the actual
convention instead of leaking one project's choices into every
deployment:

- Drop the project-wide phase/area components from the default
  filename_format, form schemas, and table columns. They must be
  all-on or all-off across a project to keep filenames lexically
  consistent, so the simplest default omits them; operators re-enable
  via the commented-out templates + a .zddc filename_format override.
  Teaching comments (incl. a field_codes: example) now ride along in
  defaults.zddc.yaml, which `show-defaults` dumps verbatim.
- Separate suffix from sequence with a template hyphen
  ({sequence}-{suffix?}); stored suffix is now just the part marker
  (A, 01) with no leading dash.
- New records: key `folder_fields: {field: parent-distance}` binds a
  body field to an ancestor folder name. The default mdl/rsk records
  bind originator to the party folder (distance 1) — the folder is the
  sole source of truth. The server overwrites the body value before
  validation + composition (WriteWithHistory and the rollup create
  path), and the form renderer marks the field read-only and pre-fills
  it. Rollup forms drop originator from required (server derives it
  from the selected party).

Tests: folder-binding overwrite + wrong-originator-filename 422, and a
form-render readOnly/prefill assertion; existing record tests realigned
so the party folder name equals the originator.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 14:31:49 -05:00
ba98b87b2a feat(roles): in-flight ratchet + auto_own_roles, drop DC subtree-admin
Two related schema/defaults changes that together replace the
admins:[document_controller] subtree-admin status with a cleaner
role-grant-via-auto-own model, and lock down the one-way handoff
through the in-flight lifecycle slots.

## New: auto_own_roles

ZddcFile.AutoOwnRoles []string is a new field on the parent's .zddc
declaring "when this directory's auto_own fires, also grant these
roles rwcda alongside the creator email". The writer
(WriteAutoOwnZddc + WriteAutoOwnZddcFenced) now takes a roles slice
and writes both the creator email AND each named role as rwcda in
the new .zddc. mergeOverlay treats AutoOwnRoles like other path-tree
contributions (leaf-wins).

The defaults' archive/<party>/ entry now sets
`auto_own_roles: [document_controller]` and drops the
`admins: [document_controller]` line:

  - When any DC mkdir's archive/<party>/, the auto-own .zddc grants
    both their email and the role rwcda. Peer DCs share full
    authority at every party without any DC needing subtree-admin
    status.
  - DCs are no longer subtree-admins anywhere. They can't bypass
    WORM (only worm-create via the worm: list) and can't reach
    inside fenced working homes. Admin elevation is reserved for
    the root admins: list.
  - Plan Review's ActionAdmin pre-flight passes for any DC via the
    role grant cascading into reviewing/ and staging/.

## In-flight ratchet (working → staging → issued)

Per-role grants at the lifecycle slots formalise a one-way handoff:

  working/   project_team: cr (create their own folders;
                              auto_own_fenced gives rwcda inside)
  staging/   project_team: cr (drop files, no modify after — the
                              "commit" step; DC takes over)
             document_controller: rwcd (transfer-to-issued needs `d`)
  reviewing/ project_team: cr (create iteration folders; auto_own
                              unfenced grants rwcda inside)
  received/  worm cr (file write-once)
  issued/    worm cr

Each handoff drops the previous role's modify rights for the slot
they pushed from. Comments in defaults.zddc.yaml document the
pattern + the "project_team drops files at staging root, never
mkdirs" convention.

## Tests

TestStandardRoles_DocControllerScopedCreate rewritten — flips
from IsSubtreeAdmin assertions to verifying:
  - rwcda at <party>/ via the auto-own .zddc (creator + role)
  - rwcda cascading to working/reviewing/ (no slot override)
  - rwcd at incoming/staging/ via explicit grants
  - cr at received/issued via WORM mask
  - IsSubtreeAdmin = false everywhere
  - DC blocked from alice's fenced working/<email>/ home

New TestStandardRoles_DocControllerMultiDC — a second DC in the
role gets the same rwcda at any party a peer created, via the role
grant in auto_own_roles.

New TestStandardRoles_ProjectTeamInFlightRatchet locks the ratchet:
project_team gets cr at working/staging/reviewing, r at incoming/
received/issued.

New TestStandardRoles_DocControllerStagingDelete confirms DC has
`d` at staging/ for the transfer-to-issued workflow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 10:51:07 -05:00
d35809cfd8 feat(forms): cascade-driven filename composition + audit on row create
Schemas:
- default-mdl.form.yaml: declare the six readOnly audit fields
  (created_at/by, updated_at/by, revision, previous_sha) so the form
  UI renders them disabled. additionalProperties: false is preserved;
  WriteWithHistory strips any client-supplied values before validation.
- default-rsk.form.yaml: overhaul to reflect the new shape. Each row
  now carries the table-tracking components (originator/phase?/project/
  area?/discipline/type/sequence/suffix?) plus a server-assigned `row`
  field; type is enum-locked to RSK to mirror the cascade's locked: rule.
  Drops the old `id` field (D-001/R-001-style identifiers are now
  composed from the components and stored in the filename).
- default-ssr.form.yaml: append the six audit fields.

Handlers:
- serveFormCreateSSR routes the write through WriteWithHistory so
  audit fields are stamped on first create (revision=1, created_*=
  updated_*=request principal/now). ssr.yaml's identity stays the
  party folder name; no filename composition runs.
- serveFormCreateRollup now resolves the cascade at the row's parent
  folder and uses the matched records: entry's filename_format to
  compose the row filename from body fields. For RSK rows the rule
  carries row_field+row_scope_fields, so the server auto-assigns the
  next sequence (001, 002, ...) within the table-tracking group and
  injects it into the body before composition. Defaults from
  field_defaults: are injected where the client omitted them
  (type=RSK locks in via the locked: list). Falls back to the
  historical date+email naming only when no records: rule is in
  scope (covers deployments that override defaults.zddc.yaml without
  declaring their own records: entries).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 09:55:07 -05:00
f3d334a221 feat(tables): rollup Add Row routes via the party column
The project-level MDL/RSK rollup specs lose `addable: false` and gain
a sibling form schema (default-project-{mdl,rsk}.form.yaml) that
makes `party` a required field. + Add row on the rollup view is now
live: the user types the party name in the Package column, the
server reads `party` from the body, validates that
<project>/archive/<party>/ exists on disk, strips the field, and
writes the row into archive/<party>/<slot>/<date>-<email>.yaml. The
response Location is the synthetic <project>/<slot>/<party>__<file>.yaml
URL so the rollup table client swaps the draft URL cleanly.

Wrong party = 422 with a clear error pointing at the SSR view as the
place to create the folder first. No auto-creation here — the rollup
is for filing deliverables/risks against existing packages, not for
spinning up new ones.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 08:14:37 -05:00
73e34bed5e feat: per-party RSK + project-level SSR/MDL/RSK rollup tables
Adds the risk register as a sibling of MDL under archive/<party>/, and
three project-level virtual aggregations at <project>/{ssr,mdl,rsk}:

  - SSR aggregates archive/<party>/ssr.yaml; "+ Add row" materializes a
    new party folder (mkdir + auto-own .zddc + ssr.yaml). Renames go
    through X-ZDDC-Op: ssr-rename, which os.Rename's the party
    directory so every row inside follows. Party name doubles as the
    folder name (no opaque IDs) and is path-derived on read.

  - MDL/RSK rollups list every deliverable / every risk across all
    parties with a derived `party` column; "+ Add row" is suppressed
    because party affiliation is ambiguous in the aggregate view.

All four virtual roots are declared `virtual: true` in
defaults.zddc.yaml. Spec/form bytes come from six new embedded
defaults (default-rsk.*, default-ssr.*, default-project-{mdl,rsk}.*)
served via a generalized IsDefaultSpec/IsDefaultSpecAbs that replaces
the MDL-only recognizer. Listing synthesis lives in fs/tree.go;
ACL on each synthetic row evaluates against the canonical
archive/<party>/ chain so non-owners see rows read-only. PUT/DELETE
through virtual URLs rewrite to canonical paths in fileapi.go via
sibling-shape blocks that don't touch the ACL gate. SSR row DELETE
returns 405 (delete the party folder via the archive view).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 21:47:56 -05:00