Commit graph

487 commits

Author SHA1 Message Date
45af24b2b1 feat(server): route no-slash directory URLs through views.dir (cascade spine)
serveSpecializedNoSlash now consults zddc.ViewAt(dir, "dir"): an explicit
`views.dir` in the cascade overrides the default_tool-derived app for the
no-slash directory URL. default_tool stays the sugar fallback (ViewAt returns
it when no views.dir is declared), so existing deployments are unaffected —
purely additive.

Also fixes the mergeOverlay trap (per the .zddc-policy-key checklist): added
Views to walker.go's per-level merge so views: survives cascade resolution at
default-driven paths (without it the key silently no-ops). Verified by a
defaults-path unit test (TestViewAt): default_tool/dir_tool surface via ViewAt;
an explicit views: entry overrides default_tool and declares a file shape.

go build + go test ./... all green. (Next: ServeView config injection from
.zddc.d/, the file→form shape, recognizer retirement, client + ./build.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 10:01:31 -05:00
760cba96c4 feat(server): add declarative views: cascade key + ViewAt resolver (schema)
Foundation for the generalized view model: `.zddc` declares, per URL shape,
which tool renders and where its supporting config lives.

- ZddcFile.Views map[string]ViewSpec{Tool, Config}; shapes "dir" / "dir_slash"
  / "file". config is a filename resolved under <dir>/.zddc.d/. Pure data — no
  behaviour; presentation/routing only (ACL/WORM/admin stay server-enforced).
- lookups.ViewAt(root, dir, shape): cascade leaf→root first-match, with
  default_tool / dir_tool honored as sugar for dir / dir_slash (semantics
  unchanged). No merged map — resolved per-shape like DefaultToolAt.
- cascade summary, isZero/is-empty checks, and validation (tool ∈ AppNames;
  config a path-bounded plain filename). Client .zddc validator (preview-yaml.js)
  gains a `views` key + `viewmap` case.

Additive only — nothing consumes Views yet (the generic resolver + dispatch
wiring + recognizer retirement follow). go build + zddc/handler tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 09:53:53 -05:00
4e86b1533d docs(server): rewrite README apps section for the local-only override model
Brings zddc/README.md in line with the apps-fetch removal:
- "Apps: virtual tool HTMLs" override section now describes the 3-tier local
  resolution (on-disk file → <root>/.zddc.zip member → embedded); drops the
  URL/channel/version spec forms, the _app/ cache, signatures, and the apps:
  example.
- Remove the ZDDC_APPS_PUBKEY env-var row.
- Security invariants #4 (in two places) reframed: a tool HTML on disk or in
  the site .zddc.zip is a full UI mount (write access = UI-mounting authority;
  <root>/.zddc.zip = site-wide), with no fetch and nothing to sign.
- Federal gap analysis: SI-7 no longer cites apps URL fetches; delete the
  whole "Code-signed apps: URL fetches (NIST SI-7)" subsection (feature gone).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 09:06:49 -05:00
4eeb25c0ef feat(server): local-only tool-HTML override; remove apps URL/version fetching
Replaces the URL/channel/version-fetching tool-HTML system with a
local-only override model. No network fetch, no Ed25519 signatures, no
channels/versions, no `apps:` .zddc key.

Tool HTML resolves, in precedence:
1. a real file on disk at the path (operator drops browse.html / archive.html
   / a new mytool.html) — served by the existing static handler;
2. an `<app>.html` member of the site-root <ZDDC_ROOT>/.zddc.zip bundle, read
   server-side via internal/zipfs (local file, no fetch, no signature;
   re-stat'd each request for free hot-reload);
3. the embedded //go:embed default.

Remove (complete unwire):
- internal/apps/{fetch,verify,cache,singleflight}.go and their tests; the
  spec-parsing/cascade machinery in apps.go (ParseSpec/Resolve/PreviewLine/
  SpecComponents/appsState, DefaultUpstream*/DefaultChannel/CacheDirName).
- --apps-pubkey / ZDDC_APPS_PUBKEY flag+env+Config field; the setupApps
  cache/fetcher/pubkey wiring (now just apps.NewServer(root, version)).
- the `apps:` / `apps_pubkey:` .zddc keys: ZddcFile.Apps/AppsPubKey, the
  walker merges, cascade-summary adds, validate.go apps validation
  (ValidateAppSourceSpec/validateURLSpec/validateChannelOrVersion/
  AppsDefaultKey/IsValidAppsKey), and the isZero/is-empty refs. A stale
  apps:/apps_pubkey: in an existing .zddc is now silently ignored
  (back-compat), not a parse error. Client .zddc validator (preview-yaml.js)
  drops the apps/apps_pubkey keys + appsmap case.

Add:
- internal/apps/bundle.go — nil-safe Bundle over <root>/.zddc.zip with
  stat-based hot-reload, size caps, corrupt-zip tolerance.
- handler.go: Server{Bundle}, resolveBytes (bundle→embedded), simplified
  Serve; X-ZDDC-Source = bundle:<m> / embedded:<app>@<ver>.
- dispatch: GET /.zddc.zip is 404 for everyone (config, not content); the
  server reads members from the filesystem internally.

Tests: new bundle_test.go (member hit/absent/no-file/hot-reload/corrupt);
handler_test.go rewritten for bundle-overrides-embedded, absent-member→
embedded, unknown-tool 503, conditional-GET for both sources; dispatch test
covers bundle override + /.zddc.zip 404 + availability rules. go build/vet/
test ./... all green; gofmt clean. Docs (AGENTS.md, ARCHITECTURE.md) updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 08:59:28 -05:00
198d691518 refine(browse): leaner menu — fold Navigate-into into Open, hide unpermitted actions, URL link in info box
Menu refinements per review:
- "Open" now navigates into a folder (rescope); the separate "Navigate into"
  item is removed. Zip → expand inline (can't navigate in); file → preview.
  Inline expand stays on single-click / chevron / arrow keys.
- "New markdown file" → "New file".
- New folder / New file / Rename / Delete are now HIDDEN when the user lacks
  the create/write/delete capability (folded into appliesTo) instead of shown
  greyed — a guest gets a lean menu; users who can still see them. New
  folder/file also remain on the toolbar.
- "Edit access rules…" is shown only when the user can actually edit them
  (admin verb 'a' or subtree/site admin) — hidden otherwise, not greyed.
- Removed "Copy path" / "Copy name" — the info box (hovercard) carries the
  name and a clickable URL now.

Info box (hovercard): dropped the on-disk "Path" row; the "URL" is rendered as
a clickable hyperlink (via the existing kvLink helper) — the shareable
reference, openable or right-click-to-copy.

Tests updated: file row omits New folder/file + Copy + Navigate; permission-
gated Rename/Delete are HIDDEN for a read-only server node and PRESENT for a
read/write/delete node (pure menuModel unit). All browse+conflict+diff green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 07:59:21 -05:00
e2179d167b feat(browse): capability/role/tier-driven, context-correct menu system
Reworks the browse menu/tree interaction into a declarative, contextually
honest model and moves view settings onto a toolbar — the menu is the UI to
the system, so it should be familiar, inviting, and only ever offer what
applies.

New declarative menu model (browse/js/menu-model.js):
- Every action is one descriptor with a TYPE predicate (appliesTo) and a
  CAPABILITY predicate (enabled)+tooltip. Row/pane menus are projections over
  it; separators are derived from group changes. Designed data-shaped so a
  future server-sourced manifest (zddc.zip) can supply/extend it.
- Hybrid visibility: type-inapplicable actions are OMITTED (New folder on a
  file, Expand on a file); permission/role/tier-gated actions are SHOWN
  DISABLED with a reason — so a lower tier sees what a higher role unlocks.
- Roles are NOT hardcoded: ordinary actions gate on the verbs the server
  returns (node.verbs / path_verbs), so any operator-defined role works. Only
  the two intrinsically-special tiers are recognised by name — site admin
  (is_super_admin) and project/subtree admin (path_is_admin), surfaced as the
  "Edit access rules…" item; both come from the existing /.profile/access.
- The headline fix: New folder / New markdown file no longer appear on file
  rows (they target a folder or the current dir).

events.js: deletes the ~350-line inline buildTreeRowMenu/buildPaneMenu/
SORT_BY_ITEMS; opens menus via menuModel projections through one openRowMenuFor
/openPaneMenu path shared by right-click, the hover kebab, and the keyboard
menu key (ContextMenu / Shift+F10). Injects action impls via menuModel.configure
to avoid a circular dep. Prefetches the scope /.profile/access (memoised) on
load/rescope/refresh/popstate so menus never fetch at open time.

Discoverability + a11y: a per-row ⋯ kebab (tree.js + new icon-ellipsis sprite,
revealed on hover/selection/focus) opens the same menu; keyboard menu key
supported.

Toolbar: Sort + Show-hidden moved OUT of per-row right-click menus into the
tree-pane toolbar, plus New folder / New file buttons (act on the current dir,
greyed with a reason when create access is lacking). Help copy updated.

Icons: dropped the 3 stray emoji from menu items (consistent, VS Code/Finder
style); only new sprite is the kebab's icon-ellipsis.

Tests: +5 browse specs (file row omits New-folder; folder row shows it; a
read-only server node greys Rename with a "write access" tooltip via a pure
menuModel unit; toolbar Sort/Show-hidden drive state + New buttons present;
kebab and Shift+F10 both open the menu). All 23 browse+conflict+diff green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 07:21:02 -05:00
8edbb81958 feat(browse): lost-update protection for editors + shared conflict dialog
Two users editing the same file online could silently clobber each other:
the editor's save did a bare PUT with no precondition, even though the master
already enforces optimistic concurrency (fileapi.go checkIfMatch → 412). Now
the editor sends a precondition and surfaces a conflict UI instead of
overwriting.

- util.js: saveFile(node, content, contentType, opts) sends `If-Match: <etag>`
  (or `If-Unmodified-Since` fallback) unless opts.force; returns {etag} from
  the PUT response (so save→edit→save adopts the new version and doesn't
  false-conflict); throws ConflictError (.status===412) on a precondition
  failure so callers branch cleanly. New saveCopy() parks a conflicting edit
  as `<stem>-conflict-<ts>.<ext>` (collision-probed) without losing either side.
- preview.js: getContentWithVersion(node) → {buf, etag, lastModified} captured
  from the content GET (the listing JSON carries no per-file etag); threaded
  into the editor ctx and exported. getArrayBuffer left untouched.
- conflict.js (new): shared, callback-driven dialog — mine-vs-theirs diff
  (reuses zddc.diff + css/history.css) + Overwrite / Reload-theirs /
  Save-a-copy / Cancel. Never calls saveFile/showFilePreview itself, so the
  deferred Phase 5 cache-outbox conflict UI can reuse it with its own callbacks.
- preview-markdown.js / preview-yaml.js: capture + forward the version token,
  adopt the returned etag on success, and on 412 open the dialog (Overwrite
  re-fetches the current etag then re-saves — re-conflicts on a third writer
  rather than blind-forcing; Reload clears dirty first so the renderInline
  guard skips its confirm). FS-Access mode sends no precondition (no
  concurrency) and never conflicts.
- build.sh: concat conflict.js after util.js.
- tests/conflict.spec.js (+ playwright project): If-Match sent, ConflictError
  on 412, new-etag returned, force omits the precondition, dialog renders the
  diff and each action resolves via its callback. Drives the fresh dist build
  over file:// with a stubbed fetch (the test binary embeds the committed
  browse.html, not dist, so a server-mode E2E would run stale code).

All browse + diff + conflict specs pass (18).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 16:24:15 -05:00
d524966f00 perf(browse): stream files into the offline zip instead of buffering all bytes
downloadFsSubtree pre-read every file's arrayBuffer() and handed the raw
ArrayBuffer to JSZip, so the entire subtree's bytes sat in the JS heap at
once before zipping — the likely OOM on a large local folder despite the
size warning. Hand JSZip the File (a Blob backed by disk) instead; it reads
each lazily during generateAsync, dropping peak memory to roughly the zip
output plus JSZip's working set.

Also document, on downloadUrl, why server-side download errors aren't
surfaced as toasts: the <a download> click is fire-and-forget, and the
folder path targets zddc-server's streamed virtual "<dir>.zip" endpoint —
routing it through fetch() to make errors catchable would defeat the
streaming for arbitrarily large archives. Left as a known, documented
limitation rather than a buffering regression.

All 6 browse Playwright specs pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 15:33:50 -05:00
b2c16063c4 refactor(browse): remove dead code, document state shape
Pure cleanup, no behavior change:

- tree.js: drop the unused setSort() method (only setSortExplicit is wired,
  via the toolbar dropdown) and its doubly-stale comment (claimed there was
  no sort UI — there is).
- app.js: remove the augmentRoot/passThroughEntries identity stub. It was a
  leftover from when browse merged virtual canonical folders client-side;
  zddc-server emits them now and nothing reads window.app.modules.augmentRoot.
- loader.js: splitExt now delegates to window.zddc.splitExtension (identical
  behavior — lowercased, dotfile/trailing-dot → '') per the CLAUDE.md rule
  that extension handling goes through window.zddc; drop the unused export.
- upload.js: remove the dead `else if (refreshUrl)` comment-only branch (and
  the unused refreshUrl var) — refreshListing is always present since it was
  exported.
- init.js: declare scopeCanonicalFolder, scopeOnPlanReview, and showHidden in
  the state initializer. They were read/written across modules but never
  listed in the canonical state shape (implicit undefined).

All 6 browse Playwright specs pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 15:32:05 -05:00
b0d0ff13cd fix(browse): serialize navigation — nav-sequence token + per-node load guard
Every async flow that ends by replacing the tree root (refreshListing,
rescopeServer, reloadDir, and the app.js back/forward popstate handler) ran
without any concurrency guard. Two overlapping listings — a double-click into
a folder, a refresh fired mid-load, rapid back/forward — could resolve out of
order, so a slow fetch would setRoot/pushState on top of a newer navigation
and leave the tree out of sync with state.currentPath and the URL bar.

Introduce a shared monotonic nav-sequence token in events.js (beginNav /
isCurrentNav, exported so the app.js popstate handler joins the same
sequence). Each flow claims a token before its fetch and bails if a newer
navigation has started by the time it resolves — last navigation wins,
stale ones drop their result before mutating anything. navigateIntoFolder's
FS branch is reordered to mutate scope state only after a successful fetch +
token check, so a bail leaves the previous scope intact instead of
half-swapped.

Duplicate-fetch race fixed at the source: tree.loadChildren took only a
`loaded` check, so rapid Enter/ArrowRight key-repeat or a double-click
landing during a single-click's load fired two concurrent fetches that raced
in setChildren. Added a `loading` in-flight flag that serializes per-node
loads — the second caller is a no-op until the first resolves. This also
removes the need to await the fire-and-forget toggleFolder calls in the
keyboard handler.

Also surfaces reloadDir fetch failures via statusError instead of swallowing
them (the success path's create/rename/delete toast no longer hides a failed
refresh).

All 6 browse Playwright specs pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 15:21:57 -05:00
bbbf5326e7 refactor(browse): consolidate duplicated helpers into util.js; fix YAML save divergence
Nine copies of escapeHtml (some escaping single-quotes + handling null,
others not), two byte-identical hashContent hashers, two saveContent
writers, two isZipMemberNode predicates, the ISO-date + YAML-quote helpers
duplicated across the workflow modals, three /.profile/access email
fetchers, and three byte-size formatters had all drifted across the browse
modules. Hoist a single browse-local window.app.modules.util (no new global;
concatenated right after init.js) and alias the call sites to it.

Reliability fix folded in: the YAML editor's saveContent skipped the
upload.ensureWritable() escalation that the markdown editor performs, so
saving a .yaml/.zddc file to a read-only-picked local folder failed where
markdown succeeded. Both now go through util.saveFile, which always
escalates — the shared writer makes the two editors impossible to drift
apart again.

Canonical escapeHtml is the strict superset (escapes & < > " ', null →
"") so it's a safe drop-in for every prior variant. fmtSize gains the GB
tier everywhere (history.js previously capped at MB). Also removes the dead
stage.js fetchSelfEmail (defined, never called).

Net −200 lines across the modules. No behavior change beyond the save fix;
all 6 browse Playwright specs pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 15:07:00 -05:00
41d4e59899 fix(browse): refresh tree after workflow moves, guard double-submit, fix modal listener leaks
Workflow data-consistency cleanup across the transmittal modules.

Stale-tree / re-trigger hazard: Stage, Unstage, and Accept reported success
with "reload to see the move" and never refreshed, leaving the moved item at
its old location in the tree — inviting the user to re-fire the action on a
folder the server had already moved. They now refresh the current listing on
success. This also revealed that events.refreshListing was never exported,
so upload.js's comment-upload refresh (which guards on it) was silently a
no-op — exporting it fixes that path too.

Non-atomic stage: "New folder" does mkdir then a separate move; if the move
failed after the mkdir succeeded the user got a generic "move failed" with an
unexplained empty folder left behind. invokeStage now tracks whether it
created the folder and says so, and refreshes so the orphan is visible.

Double-submit: Accept / Plan Review / Stage / Unstage take a module-level
busy guard so a second menu click while a POST is in flight is ignored.

Modal listener leaks (verified): the Escape keydown handler in accept,
plan-review, and create-transmittal was only removed on the Escape path —
cancel / overlay-click / submit all leaked a live document listener bound to
a detached modal. Bound once and removed in close() (matching history.js).

history.js restore: split the PUT from the post-restore refetch so a refetch
error can no longer surface a misleading "Restore failed" after the restore
has already persisted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 14:59:23 -05:00
cfb2fab401 fix(browse): editor lifecycle — dispose on switch, guard unsaved edits, kill leaks
The markdown/YAML preview editors were never disposed when switching to a
non-editor file: dispose() was only called from inside the same plugin's
render(), so md→PDF/image/YAML overwrote the pane via innerHTML and leaked
the Toast UI instance, its DOM, and document-level resizer drag listeners.
Unsaved edits were also discarded silently on any file switch (including
arrow-key auto-preview), and debounced change handlers could resolve after
an editor was disposed and write the wrong file's dirty/hash state.

preview.js now owns editor lifecycle centrally in renderInline:
- disposeEditors() up front before replacing the pane (fixes the leak for
  every md/yaml → anything switch).
- dirty guard: deliberate switches (click/Enter/menu) confirm before
  discarding; auto previews (keyboard cursor walking the tree, opts.auto)
  leave the dirty editor in place rather than nagging per keystroke;
  re-selecting the file already being edited is a no-op.
- a renderSeq token bails late-arriving loads so a slow file can't paint
  stale content into the pane after a newer selection.
- clearPreview() exposed and used by rescope (events.js) and popstate
  (app.js) so those resets dispose the editor instead of leaking it.
- beforeunload warns when an editor is dirty at page exit.

preview-markdown.js: per-mount AbortController wired into the resizer
document listeners so dispose() detaches them even mid-drag; debounced
change/save/convert handlers guard `currentInstance !== instance` so a
disposed editor's callbacks can't corrupt the active file; expose
isDirty()/currentNode().

preview-yaml.js: track dirty/node state, guard the change handler the same
way, expose dispose()/isDirty()/currentNode().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 14:46:31 -05:00
2f211d748f chore(embedded): cut v0.0.27-beta
Some checks failed
Notify chart dev on beta cut / notify-chart-dev (push) Failing after 7s
2026-06-03 13:26:23 -05:00
f7233237cd feat(server): collapse dot-guard into one admin-gated .zddc.d reserve
Replace the blanket "block every dot/underscore segment" dispatch guard
with a single reserved namespace, .zddc.d/, which is admin-only at every
depth. Everything else dot-prefixed is now ordinary ACL-governed content;
a leading dot only hides an entry from listings (UI), not from the ACL.

.zddc.d/ holds the bearer-token store, so it must stay closed even under a
broad operator grant (e.g. `*: rwcd`). The path-tree cascade has no
match-this-name-at-any-depth rule, so .zddc.d/ is gated by segment name via
a hard rule that overrides operator ACLs — on reads in dispatch (404,
existence-hidden) and on writes in authorizeAction (403 defense-in-depth
for direct callers). Token validation is unaffected: it reads
.zddc.d/tokens directly from the filesystem in ACLMiddleware, before the
HTTP-layer gate.

The segment match is case-insensitive (strings.EqualFold): ZDDC_ROOT may
sit on a case-insensitive filesystem (SMB/CIFS/Azure Files) where .ZDDC.D
resolves to the same dir, so a write to a case-varied path — e.g. a MOVE
destination header that skips dispatch's canonical case-folding — must not
slip past the gate and plant a forged token. The dispatch gate also runs
BEFORE the raw .zddc view so the reserve's own cascade
(/<dir>/.zddc.d/.zddc) is existence-hidden rather than leaked by
ServeZddcFile. Regression tests cover both.

To keep all bookkeeping inside the one reserve, relocate the last two
caches under it (both regenerable, no data migration): the apps cache
_app/ -> .zddc.d/apps/ and the per-directory MD-conversion cache
<dir>/.converted/ -> <dir>/.zddc.d/converted/.

New internal/handler/sidecar.go defines ReservedSidecar + the
HasReservedSidecar / ActiveAdminForSidecar predicates used by both the
dispatch read-gate and the write-path gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 13:23:00 -05:00
b59a7f6100 chore(embedded): cut v0.0.27-beta
Some checks failed
Notify chart dev on beta cut / notify-chart-dev (push) Failing after 8s
2026-06-03 12:46:12 -05:00
29182480c2 feat(scripts): add migrate-toplevel-peers.sh
Idempotent migration from the old archive/<party>/<slot> layout to the
flat top-level party-peer layout: moves the workspace/register slots
(incoming/working/staging/reviewing/mdl/rsk) out to <project>/<slot>/<party>/,
moves archive/<party>/ssr.yaml → ssr/<party>.yaml (the party registry),
synthesizes a minimal ssr/<party>.yaml for any archived party that lacks
one, and leaves archive/<party>/{received,issued} in place (the WORM
record). --dry-run flag; per-party summary; tested + idempotent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 12:37:04 -05:00
f94defc8c1 feat(browse,tables): flat-peer clients + dual-mode cross-party aggregate
browse: the party picker reads the ssr/ registry (the authoritative party
list) and creates at physical peer paths <project>/<peer>/<party>/…;
"register new party" writes ssr/<party>.yaml first (party_source: ssr).
stage.js + accept-transmittal.js repointed to the top-level workspace peers
(working/staging/incoming) — received/issued + plan-review stay under the
WORM archive.

tables: mdl/ and rsk/ render the cross-party aggregate by recursing ONE
level into the party subdirs CLIENT-side (works online AND offline), with
$party from the server-injected row content (or derived from the subdir
offline). Rows carry the <party>/ prefix so reads/edits hit the real
per-party path. The server just lists the peer root normally (party subdirs
+ synthetic table.yaml/form.yaml) — the fs/tree flattening + ListRollupRows
are dropped in favour of this dual-mode client recursion.

Full Go suite + all 256 Playwright tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 12:35:31 -05:00
bee36c2ee9 test(handler,cmd): update suites for flat-peer layout
Repoint handler + dispatch tests to the top-level peer layout: register
parties via ssr/<party>.yaml where party_source gates writes; move
workspace paths out from under archive (incoming/working/staging/reviewing
+ mdl/rsk are top-level, archive/<party>/{received,issued} stay WORM);
rewrite SSR create (writes ssr/<party>.yaml, no archive folder) + SSR
rename (registry-only); accept-transmittal source incoming/<party>/<txn>;
plan-review scaffolds top-level reviewing/staging; tablehandler
classifyVirtualTableDir recognizes <project>/<peer>/<party> (depth-3) for
per-party mdl/rsk tables. Full Go suite green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 12:15:56 -05:00
150da9d186 test(apps,fs): update availability + listing tests for flat-peer layout
availability_test: tools resolve via the peer cascade (classifier on
incoming/working/staging, transmittal on staging, tables on mdl/rsk/ssr).
tree_test: drop the abandoned per-user-home + folder-nav virtual tests;
add an mdl/ cross-party aggregate-listing test; repoint empty-when-missing
to the declared peers.
2026-06-03 11:51:29 -05:00
7d462ab7a4 test(zddc): update internal/zddc suite for flat-peer layout
Repoint default-tool/history/canonical-folder/auto-own/virtual/declared,
role-grant, and WORM-zone expectations to the top-level peer layout: archive
is now blanket-WORM (DC = rc there), the workspace/register peers carry the
DC grants directly, and incoming/working/staging/reviewing/mdl/rsk/ssr are
physical peers. ensure_test repointed to top-level paths + the virtual-reject
test inverted (peers are physical now).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 11:47:04 -05:00
db110665f0 feat(server): flat top-level party peers + pure-WORM archive (impl)
Reshape the project layout from "archive/ is the only physical dir + six
virtual aggregators" to a flat set of physical, party-partitioned peers:

  archive/<party>/{received,issued}   pure WORM (one rule, no exceptions)
  incoming|reviewing|working|staging/<party>/   workspaces
  mdl|rsk/<party>/*.yaml              registers (cross-party aggregate at the
                                      peer root, $party from the real subdir)
  ssr/<party>.yaml                    submittal status register AND the
                                      authoritative party registry

A party exists iff ssr/<party>.yaml exists; the new `party_source: ssr`
cascade key gates party-folder creation under every other peer (archive
included) — create <peer>/<party> only when the registry row exists, else
409. Registration is a plain create of ssr/<party>.yaml (no WORM gymnastics),
so archive/ stays purely WORM.

Server core:
- defaults.zddc.yaml rewritten to the flat-peer + WORM-archive + party_source
  shape; every virtual: removed; mdl/rsk get document_controller rwcd.
- slots.go: projectPeers/IsProjectPeer; perPartySlots={received,issued}.
- party_source key end-to-end (file.go/walker/lookups/cascade) + PartyRegistered.
- ensure.go canonical-ancestors generalized to peers; virtual reject removed.
- virtualviews.go: deleted the virtual-URL resolver/types/regex; kept
  ListParties (reads ssr/*) + repointed ListRollupRows (physical <peer>/*/*).
- fs/tree.go: mdl/rsk peer-root listing aggregates physical party subdirs
  (replaces the subdir folder-nav); ssr flat; spec entries advertised.
- fileapi.go: deleted virtual PUT/DELETE rewrites; mkdir allowlist → peers;
  partySourceGate on mkdir/PUT/move.
- virtualviewhandler.go → ServeInjectedRow ($party/name injected on read so
  the tables client renders the column unchanged).
- ssr/form/table handlers repointed to real paths (SSR create writes
  ssr/<party>.yaml; rollup create writes mdl|rsk/<party>/<file>.yaml; SSR
  rename is registry-only); IsDefaultSpec recognizes the new spec locations.
- accept-transmittal source incoming/<party>/<txn> (+ PartyRegistered guard);
  plan-review scaffolds top-level reviewing/<party> + staging/<party>.
- main.go dispatch: removed virtual-row GET + folder-nav 302; injects the
  source column on register-row reads.

Non-test build is green. Test suites still assert the OLD layout (verified:
all current failures are stale expectations, not bugs) — the test rewrite,
browse/tables client updates, and the data-migration script follow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 11:40:09 -05:00
8875d490f5 chore(embedded): cut v0.0.27-beta
Some checks failed
Notify chart dev on beta cut / notify-chart-dev (push) Failing after 8s
2026-06-03 08:55:39 -05:00
3e8737b7c9 feat(browse): preview .docx/.xlsx + fix markdown-editor horizontal overflow
DOCX/XLSX preview: add renderDocx (docx-preview) and renderXlsx (SheetJS)
to shared/preview-lib.js — the natural home alongside renderTiff/
renderZipListing, reusable by every tool. browse dispatches Office files
to them in both the inline pane and the pop-out window via the existing
preview.isOffice() check, and browse/build.sh now bundles the
docx-preview + xlsx vendors. Renderers degrade to a friendly message if a
tool doesn't bundle the vendor.

Overflow fix: .md-shell used `grid-template-columns: 280px 1fr`. A bare
`1fr` is `minmax(auto, 1fr)`, whose `auto` floor is the editor's
min-content width (Toast UI's toolbar) — so the content track refused to
shrink and the whole shell overflowed #previewBody as the window narrowed
instead of the editor reflowing smaller. Switch both tracks to
minmax(0, …) in the CSS and in the three JS spots that rewrite the
columns on sidebar-drag, and give .md-shell__sidebar min-width: 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 08:55:14 -05:00
c05fc376f2 chore(embedded): cut v0.0.27-beta
Some checks failed
Notify chart dev on beta cut / notify-chart-dev (push) Failing after 9s
2026-06-02 14:01:29 -05:00
af07fa4f80 Merge feat/md-history-perfile: admin URL toggle, per-save markdown history, slot registry/history_globs, history→.zddc.d/
Lands the v0.0.27 feature stack: ?admin= URL elevation toggle, the per-save
edit-history redesign (now stored under .zddc.d/history/), the canonical-slot
registry refactor + history_globs cascade key, and the .devshell removal.
2026-06-02 14:01:09 -05:00
e4e0fedaa2 refactor(history): store under .zddc.d/history/; drop .history carve-out + dead .devshell
Consolidate edit-history bookkeeping under the single reserved .zddc.d/
sidecar (where tokens + access logs already live), instead of its own
top-level .history/ dot-name:

- history.go: record + text history now write/read <dir>/.zddc.d/history/<stem>/
  (was <dir>/.history/<stem>/). Const renamed .history → .zddc.d/history and
  unexported (the only external user was the dispatch carve-out). The history
  VIEWER endpoints (<record>.yaml?history=1, <file>?history=…) read it
  server-side, so they keep working for anyone with read on the live file;
  the raw store is bookkeeping, blocked by the existing dot-prefix guard.
- main.go: drop the .history GET carve-out (b9ebee7) — superseded; history is
  reached via the viewer, not raw browsing. Reword the guard comment to
  "reserve .zddc.d/ bookkeeping" (Part B will replace the blanket block with a
  .zddc.d/ admin-fence).
- Delete dead .devshell references (the dev-shell was dropped from the chart):
  guard comment, paths.go comment, test fixtures/cases (→ .zddc.d), and docs.

This is Part A of the approved plan: ship history in its permanent home so we
never migrate it twice. Tests updated to the new paths; the obsolete
TestDispatchHistoryReadCarveOut is removed (raw-block covered by
TestDispatchHidesDotPrefixedSegments, viewer by mdhistory_test).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 13:48:41 -05:00
1eeaa1bd96 refactor(zddc): centralize canonical-slot registry; feat: history_globs cascade key
Two cleanups from the hard-coded-vs-cascade audit:

#2 Centralize the canonical slot names. The lists {ssr,mdl,rsk,working,
staging,reviewing} and the per-party {incoming,received,issued,mdl,rsk,
working,staging,reviewing} were hand-written across ensure.go (×2),
fileapi.go (×2), virtualviews.go, lookups.go. New internal/zddc/slots.go is
the single registry with IsRowSlot/IsFolderNavSlot/IsVirtualAggregatorSlot/
IsPerPartySlot; virtualViewRE is built from it. Slot NAMES stay hard-coded
(they carry bespoke behavior) but now live in one place — adding/adjusting a
slot is one edit, not a hunt. Pure refactor; behavior unchanged.

#1 Make the history file-type selection cascade-driven. IsTextHistoryCandidate
hard-coded ".md"; now it matches the effective history_globs from the .zddc
cascade (default ["*.md"], widen per-deployment e.g. ["*.md","*.txt"]). New
ZddcFile.HistoryGlobs + mergeOverlay + PolicyChain.EffectiveHistoryGlobs +
HistoryGlobsAt, threaded through serveFilePut/serveFileMove/dispatch and
ServeTextHistory (now takes fsRoot). The history: bool still gates whether
snapshots are recorded; history_globs only says which file types qualify.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 10:50:53 -05:00
b9ebee7551 fix(history): serve .history snapshots as ACL-gated content (carve dot-prefix guard)
Clicking a history snapshot in the tree 404'd: the dispatcher's dot-prefix
guard blocks every .-segment URL, and the preview fetch hit the raw
.history/<stem>/<snap>.md path. But .history is ACL-modeled content (it
inherits the shadowed file's .zddc chain), not infra like .devshell — so
the guard was redundant with permissions there.

Carve GET/HEAD of .history out of the dot-prefix guard: snapshots are now
fetchable as ordinary ACL-gated files (read the live file → read its
history). Writes into .history stay blocked, and the listing dot-filter
still hides it from default views unless ?hidden is set. Export
handler.HistoryDirName for the dispatcher.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 10:16:12 -05:00
90a0901951 Merge feat/admin-url-toggle into md-history test branch
Combine the ?admin=true URL elevation toggle with the per-save history
redesign so both can be exercised together on bitnest before landing.
2026-06-02 09:59:47 -05:00
7ff78ef254 feat(history): self-describing per-save snapshots + readable-when-disabled + mdl/rsk/working defaults
Redesign the markdown edit-history store from content-hashed blobs +
log.jsonl to one self-describing file per save:

  .history/<stem>/<ts>-<email>.<ext>

The filename IS the audit (colon-free UTC timestamp valid on SMB/Azure
Files + the authoring email); listing the directory is the history. No
sidecar log, no hashing. A byte-identical save is a no-op; a pre-existing
file lazy-seeds its current bytes (author "unknown", stamped at mtime).
Reverting copies an old snapshot back (records as a fresh save). Snapshots
are kept forever.

Fixes the 404 reading history: reads no longer require history to be
*currently* enabled — ServeTextHistory serves whatever .history/<stem>/
exists (empty list when none); the dispatch drops the EffectiveHistory
gate for reads. WRITES stay gated by the history: flag. (The 404 came from
the aggregator refactor turning history off on project-level working/,
which made already-recorded snapshots unreadable.)

Renames: an in-place rename carries .history/<stem>/ to the new name
(serveFileMove); a cross-dir move leaves it behind.

Defaults: history: true now ships on the three live-editing slots —
working, mdl, rsk — at both the project-level nodes and the per-party
folders. It's a .zddc cascade key, so operators override per project.
Records (.yaml in mdl/rsk) keep their separate record-history path.

Browse history viewer updated to the filename-based version id (id ←
sha). Tests rewritten for the per-file scheme + rename behavior + SMB-safe
names; HistoryAt defaults test updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 09:51:23 -05:00
143e26f337 feat(elevation): toggle admin mode via ?admin=true|false URL param
Replace the header "Admin" checkbox with a URL toggle reachable from any
zddc-server page: ?admin=true arms, ?admin=false (or the red banner's "Drop
admin" button) drops. Reuses the existing zddc-elevate cookie as the sticky
state — it persists across navigation for its Max-Age window and is what the
server reads — so the param is stripped from the URL once applied (via
location.replace, so Back can't re-trigger it and it isn't bookmarked).

Arming is gated on /.profile/access can_elevate: a non-admin who types
?admin=true just gets the param stripped, never a misleading red border.
The red viewport border + banner (applyArmedChrome) are unchanged and still
reflect the cookie on every page load. The now-unused #elevation-toggle
header span stays hidden in templates (inert); render() removed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 08:31:40 -05:00
28ebaa19cd chore(embedded): cut v0.0.27-beta
Some checks failed
Notify chart dev on beta cut / notify-chart-dev (push) Failing after 8s
2026-06-01 13:31:01 -05:00
5ed4f8582b Merge feat/effective-access-hovercard: per-location permissions/roles, copyable read-only YAML, scoped access endpoint
- browse hovercard shows 'Your permissions' (verbs) + 'Your roles' (cascade
  roles) for the hovered folder/file; server adds path_roles to
  /.profile/access?path=.
- read-only YAML/.zddc viewer is selectable/copyable (readOnly:true, not
  'nocursor') without enabling admin mode.
- /.profile/access?path= returns only the path-scoped payload (skips the
  global project + admin-subtree tree walks).
2026-06-01 13:30:46 -05:00
1cf3f3a9b3 perf(server): scope /.profile/access?path= to the requested location only
enumerateAccess always computed the global summary — every project
(EnumerateProjects) and every admin subtree (enumerateAdminSubtrees tree
walk) — and merely appended the path-scoped fields when ?path= was given.
The browse hovercard calls this per folder hovered, so each distinct folder
paid a full global enumeration for data it never reads.

Split the two: a ?path= query now returns ONLY identity + path_verbs/
path_is_admin/path_can_elevate_grant/path_roles and skips the tree walks;
the no-path call still returns the full global view for the profile page.
Verified all path-scoped consumers (browse hovercard, form, tables) read
only path_* fields; the global consumers (elevation, stage, plan-review,
accept-transmittal) all call without ?path=.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 13:23:22 -05:00
91e6d1ec82 fix(browse): keep read-only YAML/.zddc selectable & copyable
The YAML viewer mounted CodeMirror with readOnly:'nocursor', which removes
the textarea from focus and kills click-drag selection — so copying a
read-only .zddc snippet was impossible without flipping on admin mode to
make it writable. Use readOnly:true instead: non-editable but focusable,
so the user can select and copy. autofocus:false still keeps arrow-key
tree navigation intact until they click into the editor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 13:13:20 -05:00
e258b0fa3d feat: show effective permissions + roles per location in the browse hovercard
Hovering a folder/file now shows "Your permissions" (the rwcda verbs you
hold there) and "Your roles" (the cascade roles you're a member of at that
location — e.g. document_controller, project_team). Roles are cascade-
scoped, so they can differ by location; this answers "does the system think
I'm a document_controller here?".

- server: RolesForPrincipalInChain(chain, email) resolves the caller's role
  memberships at a path (honouring fences/resets, incl. embedded standard
  roles); /.profile/access?path= now returns path_roles alongside path_verbs.
- browse hovercard: "Your permissions" from node.verbs (sync); "Your roles"
  async-filled from /.profile/access?path= via zddc.cap.at (memoised).
  Offline mode shows "local folder (filesystem)" and no roles row.

Tests: RolesForPrincipalInChain unit tests (member union, wildcard members,
non-member, fence-hides-ancestor-role, empty email).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 11:12:39 -05:00
303bf7aade release: v0.0.26 lockstep
Some checks failed
Notify chart dev on beta cut / notify-chart-dev (push) Successful in 7s
Build + deploy releases / build-and-deploy (push) Successful in 22s
Build + deploy releases / notify-chart-prod (push) Failing after 7s
2026-06-01 10:52:10 -05:00
23f4edffdc Merge feat/aggregator-party-picker: aggregator party-picker + offline CRUD
Browse 'New folder/file' at a virtual aggregator root (working/staging/
reviewing) now prompts for a party and targets archive/<party>/<slot>/
(with a DC-gated '+ New party' option); the server 409s a direct mkdir in
an aggregator instead of shadowing. Reverts the project-level creator-owned
working/ folders in favour of uniform party-scoping. Local-directory mode
gains full create/edit/rename/delete via the FS-Access API with lazy
readwrite escalation.
2026-06-01 10:51:46 -05:00
96262a171e feat(browse): full create/edit/rename/delete in local-directory (offline) mode
Local folders are picked read-only, so create/edit/rename/delete were
either disabled or would fail. Now offline mode supports the same CRUD as
server mode, bounded only by what the filesystem grants:

- upload.js: ensureWritable() escalates the picked root to readwrite via
  the FS-Access permission prompt on the first mutation (one prompt, then
  granted for the session; requires the user gesture every caller has).
  makeDir/makeFile gain FS-API branches (getDirectoryHandle/getFileHandle
  {create:true} + createWritable) resolved through handleForDir; removeNode
  and renameNode (already FS-API) now escalate first.
- preview-markdown.js: the markdown editor's save escalates before
  createWritable, so editing a local .md persists.
- events.js: New folder / New markdown file menu items are enabled whenever
  there's a writable target (server, or a picked local folder) via
  canCreateHere(); rename/delete were already gated by canMutate (FS-API).
  The aggregator party-picker stays server-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 10:46:51 -05:00
56c3353f7b feat(browse): party picker for New folder/file in virtual aggregators
Creating a folder/file at a project-level folder-nav aggregator root
(working/staging/reviewing) used to error or silently shadow — the slots
are virtual and content is party-scoped. Now browse opens a party picker
that targets archive/<party>/<slot>/<name>, with a "+ New party…" option
(server-gated to the document_controller via the existing archive/ ACL).

- events.js: aggregatorRoot detection + openPartyPicker modal (mirrors the
  stage.js modal), createInAggregator routes the create to the canonical
  archive path; rewriteAggregatorPath handles right-clicking a party row
  shown in an aggregator listing so it never re-prompts.
- server: serveFileMkdir now 409s a mkdir inside an aggregator
  (rejectProjectAggregatorMkdir) with a pointer at archive/<party>/<slot>/,
  instead of letting the write fall through to an unreachable shadow dir.

Reverts the prior session's project-level creator-owned working/ folders
(per the design decision to make all three folder-nav slots uniformly
party-scoped): working/ is a pure virtual aggregator again like
staging/reviewing — drops the working/ history+auto_own+acl defaults, the
EnsureCanonicalAncestors working exception, the working-root document-
controller file gate (serveFilePut/Move) and zddc.IsRoleMemberAt. Per-party
archive/<party>/working/ keeps its own history + auto-own.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 10:39:49 -05:00
0a7f8594c5 release: v0.0.25 lockstep
Some checks failed
Notify chart dev on beta cut / notify-chart-dev (push) Successful in 8s
Build + deploy releases / build-and-deploy (push) Successful in 21s
Build + deploy releases / notify-chart-prod (push) Failing after 7s
2026-06-01 10:07:02 -05:00
db96333718 Merge feat/md-history: markdown edit-history + creator-owned working folders
Server-side edit-history versioning for working-folder markdown (content-
addressed .history store + browse version-history viewer with diff/restore),
and the creator-owned working-folder model with document-controller-gated
files at the working/ root.
2026-06-01 10:06:25 -05:00
0d21c16102 feat(server): creator-owned working folders; document-controller-gated root files
Replace the project-level working/<email> "personal workspace" idea (too much
complexity for too little) with a simpler model on the virtual <project>/working/:

- EnsureCanonicalAncestors now materialises the working/ slot dir on disk the
  first time real content is created beneath it (it stays a plain dir, never
  auto-owned). ssr/mdl/rsk/staging/reviewing keep rejecting physical writes.
- Each <project>/working/<folder>/ a user creates gets an unfenced auto-own
  .zddc (creator rwcda; the team inherits read+create-new, not w/d). history:
  true still inherits in, so markdown drafts there are versioned.
- defaults grant project_team rc + document_controller rwc at working/ so users
  can create their folders and the DC has authority throughout.
- A bare file DIRECTLY at the working/ root is reserved for the
  document_controller: serveFilePut and serveFileMove reject non-DC writes/moves
  there (isProjectWorkingRootFile + zddc.IsRoleMemberAt), independent of the ACL
  verb since mkdir and file-PUT both authorise as ActionCreate. Users work inside
  a folder; the DC creates files at the root or promotes one up with a MOVE.

Tests: ensure_test materialisation + plain-slot cases; fileapi_test DC-gate for
PUT and MOVE. The generic dispatch-routing test moves its ops into working/drafts/.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 10:05:26 -05:00
5b8bcaed89 chore(embedded): cut v0.0.25-beta 2026-05-29 14:37:10 -05:00
c489a78f34 feat(server): enable edit-history on the project-level personal workspace
Per the working-folder design: <project>/working/<email>/ is each user's
personal workspace (public by default, owned by the creator who can privatize
via .zddc). The post-reshape defaults had stripped that node to a bare
aggregator, so personal markdown drafts got no history. Add history: true +
an auto_own (un-fenced) per-user-home rule to the project-level working node.
archive/<party>/working/ keeps its own history: true. Scope stays working-only
(staging/reviewing unchanged).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 14:36:56 -05:00
e58e66a49c chore(embedded): cut v0.0.25-beta 2026-05-28 14:20:21 -05:00
9972e6773a feat(browse): markdown version-history viewer with diff + restore
Adds a "History…" context-menu item on markdown files in a history:true
subtree (server mode only — the audit is server-stamped). It opens a modal
that lists every saved version newest-first (timestamp + author + size,
current flagged), lets you View any version, Diff any two, and Restore one
(a forward PUT — non-destructive).

- shared/diff.js: dependency-free line/word LCS diff (window.zddc.diff),
  prefix/suffix trimming + a cell cap so large files don't stall the UI.
- browse/js/history.js: the modal (list / view / diff / restore), talking to
  GET <url>?history=1 and ?history=<sha>.
- loader.js carries the per-file history flag; events.js adds the menu item.
- Wired diff.js + history.js + history.css into browse/build.sh; diff.js into
  the zddc-test.html shim. tests/diff.spec.js covers the diff algorithm.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 12:49:00 -05:00
d00afa1ddc fix(server): carry history through the paths-tree merge
mergeOverlay (used to thread embedded defaults' paths: tree into chain
levels) didn't copy the new History *bool, so EffectiveHistory never saw
history: true on archive/<party>/working/ — the feature would have silently
never triggered. Add the field to the overlay and a HistoryAt defaults test
that exercises the real cascade (working/ + fenced homes true; sibling slots
false).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 12:48:49 -05:00
6efe71e573 feat(server): edit-history versioning for working-folder markdown
A history: true .zddc subtree (enabled by default on archive/<party>/working/)
routes markdown PUTs through WriteTextWithHistory: each save snapshots the
content into a hidden, immutable .history/<stem>/ store (content-addressed
blobs + an append-only log.jsonl carrying server-stamped {ts, email, sha,
prev}) before writing the live file. The live file at its natural path stays
the source of truth; no symlinks, no audit in the body/filename.

Reads: GET <file>?history=1 lists versions (newest-first, current flagged);
GET <file>?history=<sha> returns that version's bytes (hex-id guard against
traversal). Listings carry a per-file History flag so the browse client knows
where to offer the affordance.

History is subtree-inheriting and ignores inherit:false ACL fences (versioning
is a write behavior, not a permission), so fenced per-user homes under working/
are covered too. No-op saves dedup; pre-existing files lazy-seed their origin
version. Records (.yaml) keep their existing in-body-audit history path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 12:37:51 -05:00