A table column can declare `options_source: <peer>` and the server fills its
`enum` from the live entries under <project>/<peer>/ — so the row editor renders
a dropdown of the current registry instead of free text. Generic + configurable
in the spec; no hardcoding.
- Server (tablehandler.go): resolveDynamicEnums + registryEntries resolve the
peer directory (its *.yaml basenames + subfolders, sorted, dot/spec entries
skipped) into the column enum at ServeTable time, before the context inject.
- Default risk register: add a `package` column with `options_source: ssr`
(dropdown of the project's SSR packages) + the matching form property. The
spec comment documents the key so operators can source other registries.
- Test covering the resolver (entries, skips, untouched columns).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>
Adds history_test.go with eight cases exercising the record-write
orchestration path:
- CreateStampsAuditFields: PUT to a fresh mdl path → audit fields
injected; response echoes the stamped YAML; no history dir yet.
- UpdateIncrementsRevisionAndArchivesPrior: second PUT archives
the prior bytes under .history/<base>/<ts>-<sha8>.yaml, bumps
revision, preserves created_*, chains previous_sha.
- ConflictPreservesHistory: 412 from stale If-Match leaves the live
file untouched and writes NO history entry (the failed write must
be a true no-op).
- ClientAuditFieldsStripped: client-supplied created_by / revision
are silently overwritten by server values — anti-forgery test.
- FilenameMismatch: URL says ...-0002 but body composes to ...-0001
→ 422.
- LockedFieldRejected: posting type=SPC to an rsk row → 422 with
/type error (rsk/ locks type=RSK via cascade).
- SSRHistoryAtPartyLevel: writes to archive/<party>/ssr.yaml put
history at archive/<party>/.history/ssr/, NOT at
archive/.history/<party>/.
- RollupCreate_AssignsRowAndComposesFilename: three POSTs to
/project/rsk/form.html in two table-scope groups demonstrate the
server picks up filename_format + row_field+row_scope_fields from
the cascade, auto-assigns sequence row numbers per group, and
composes the canonical filename.
Bug fix surfaced by the first test: composeFilename was eliding TWO
separators around an optional placeholder when one was correct.
"ACM-{phase?}-PRJ" with phase="" was producing "ACMPRJ" instead of
"ACM-PRJ". Now drops only the trailing separator from output and
lets the next iteration emit the connector.
Default-project-{mdl,rsk}.form.yaml updated: project-rollup MDL +
RSK schemas gained the six readOnly audit fields and the project-
rsk schema picked up the full table-tracking component shape (+
row) plus an enum-locked type=RSK. The required: list no longer
includes type for rsk schemas — the cascade's field_defaults
injects it after schema validation, and requiring it would 422
well-behaved clients.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>