Skip to content

[judy] feat(refs): editable .md refs + ETag + danger banner + paper-trail#48

Merged
SymbolStar merged 2 commits into
mainfrom
judy/ref-edit-md
May 28, 2026
Merged

[judy] feat(refs): editable .md refs + ETag + danger banner + paper-trail#48
SymbolStar merged 2 commits into
mainfrom
judy/ref-edit-md

Conversation

@SymbolStar

Copy link
Copy Markdown
Owner

Implements PRD v1.1 + design v0.1 from thread th_19e6d36bf3b_7be92c.

Scope (per Alice PRD / Judy tech plan / Dora design):

  • .md refs only — all editable, no per-file writable flag needed.
  • ETag = sha256(content)[:16]. Both ETag and X-Ref-Etag headers.
  • PUT /api/refs/<id>/content body {content, actor?, thread_id?} + If-Match header.
  • 409 returns {current_etag, current_content} for 3-way UI decision.
  • Atomic write (tmp + fsync + os.replace).
  • AC-10 paper trail: when thread_id is supplied, post ✏️ <actor> 于 HH:MM 编辑了 <label>(+N / -M 行) as __editor__. Router is bypassed (no @mention fan-out from inside the file body).
  • AC-9 danger banner: STATUS/MEMORY/AGENTS/SOUL/IDENTITY/USER.md basenames show a non-blocking yellow warning above the textarea; agent id resolved from workspace-<agent> segment.
  • IME guard: ⌘/Ctrl+S during composition does not save.

Out of scope (PRD non-goals, design checklist): CodeMirror, rich text, collab cursors, version history, diff view, autosave. v1 is textarea + Save.

Tests: 510/510 green. Replaced requires_writable with md-always-editable + non-md-still-readonly + etag-conflict (unit + HTTP).

…em-post paper trail

PRD: alice/PRD.md v1.1; Design: designer/design-v0.1.md
Thread: https://github.com/SymbolStar/OpenForge (th_19e6d36bf3b_7be92c)

Backend (forge_refs.py / server.py):
- .md refs are now editable regardless of writable flag (per PRD).
- GET /api/refs/<id>/content returns ETag (sha256(content)[:16]) in
  both 'ETag' and 'X-Ref-Etag' headers.
- PUT /api/refs/<id>/content now expects JSON body
  {content, actor?, thread_id?} and honours 'If-Match' header.
  - 409 returns {current_etag, current_content} so the UI can decide.
  - atomic write (tmp + fsync + os.replace), diff (+N / -M) returned.
  - if thread_id supplied: appends a system 'paper-trail' post by
    speaker='__editor__' to the thread (bypasses post router to avoid
    fan-out on @mentions inside the file).
- Non-.md still 403 (read-only) unless ref.writable=True (legacy).
- New RefConflictError class.

Frontend (web/app.js):
- Ref viewer wires Edit tab for .md (greyed for non-md).
- Tracks state.refEtag across GET/PUT; sends If-Match.
- 409 -> confirm() with both etags; choose 'force' or 'discard mine'.
- Danger banner above textarea for STATUS/MEMORY/AGENTS/SOUL/IDENTITY/USER.md
  (basename match); not blocking, only warns. Resolves agent id from
  workspace-<agent> in abs_path.
- IME guard: Cmd/Ctrl+S during composition does NOT trigger Save.

Tests:
- Replaced legacy 'requires writable' test with .md-always-editable +
  non-md-still-readonly + etag-conflict (unit + HTTP).
@github-actions

github-actions Bot commented May 28, 2026

Copy link
Copy Markdown

🤖 bot-review (comment-only · phase 1)

Diff: 6 files changed, 454 insertions(+), 222 deletions(-) @ 29f5b97

Red-line checks:

  • ✅ A-7.5: no new 'forbidden' code in xiaof

Needs human review — these paths are not eligible for future auto-approve:

  • forge_xiaof.py (xiaof contract — verify A-7.5/7.6 invariants)
  • server.py (HTTP routes / handler — never auto-approve)

Phase 1: this bot leaves comments only. Auto-approve will be enabled per-path after 1–2 weeks of clean runs. Promotion plan: judy PR #42 follow-up.

Codex review (PR #48) flagged two 🔴 data-corruption risks + one 🔴
semantics drift. Fixed all three:

1. **Frontend stale ref state** (web/app.js): selectFile() now clears
   state.currentRef / refEtag / isMdRef. Without this, opening a .md
   ref then switching to a workspace file would make saveCurrent()
   wrongly route to saveRef() and PUT workspace content into the old
   ref id.

2. **Backend tmp-file collision + ETag TOCTOU** (forge_refs.py): The
   atomic write used a fixed '<name>.tmp' that two concurrent PUTs in
   the same dir could clobber, AND the If-Match check was not under
   any lock — two writers holding the same ETag could both pass the
   check and the loser silently overwrote. Now:
   - per-ref in-process Lock wraps re-read + check + replace
   - tmp name is unique (pid + 4 bytes random) and opened O_EXCL
   - file-size re-check after lock (avoid OOM on external growth)
   - best-effort parent-dir fsync for durable rename
   - tmp cleanup on failure

3. **Non-md semantics** (forge_refs.py + tests): Removed the dead
   'non-md still honours writable' branch. v1 PRD is .md only — both
   writable=False and writable=True non-md now reject as
   RefValidationError (HTTP 400). Test updated.

Other nits from codex review picked up:
- Danger-banner regex now matches workspace-<agent> at path-segment
  boundary, includes '.' (mirrors server agent-id allowlist).
- Cmd/Ctrl+S in edit view now ALWAYS preventDefault (so browser
  'save page' never escapes), then skips on IME composition.
- New unit + HTTP tests assert: exact sha256[:16] ETag, file
  unchanged on conflict, quoted If-Match strip works, writable=True
  non-md still rejected.

All 511 tests green.
@SymbolStar SymbolStar merged commit 5fd0a07 into main May 28, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant