Skip to content

Draft file and draft-to-description (Stage 1) - #221

Merged
guijacquemet merged 2 commits into
mainfrom
claude/description-draft-file
Nov 8, 2026
Merged

guijacquemet merged 2 commits into
mainfrom
claude/description-draft-file

Conversation

@guijacquemet

Copy link
Copy Markdown
Contributor

What

  • <output>/morphobricks.draft.json: a versioned, strictly read JSON file with the user's answers so far, base_sha256 (hex of the published bytes, or "absent") and the pinned Spec commit. Saved atomically under output_lock (run/draft_file.save_draft). A malformed, unknown-version or extra-key file is refused with DraftFileError (code: malformed, unknown_version, unknown_key, R_DRAFT_BASE_MISSING, R_FILE_SYMLINK), never repaired.
  • Resume (load_draft): refused with R_PUBLISHED_FILE_CHANGED when the published file is no longer the recorded base (also when it is gone or the base was absent and a file appeared); R_DRAFT_BASE_MISSING when the file has no usable base. An answer for a folder the draft no longer has is refused (unknown_key), never moved.
  • Draft to description (draft_answers.description_from_draft, pure, no clock): refuses with DraftIncomplete listing every missing confirmation once (id_pending, repeat_not_confirmed, confirmed_on_missing, control_unresolved, legacy_folder_missing, unit_channels_missing, channel_undeclared); otherwise returns the Description. Nothing is adopted from a proposal.
  • publish_draft: under one hold of the lock, resume, build, publish with every check of publish_description (symlink, base, transitions, strict reader on the exact bytes), then delete the draft. Any refusal writes nothing. run/description_file.publish_locked is publish_description for a caller that already holds the (non re-entrant) lock; publish_description now calls it.
  • Contract §18 documents the format.

Why

The Stage 1 design (section "Draft / confirmed lifecycle", module table) specifies the draft file and the publication step, but no merged PR built them. #220 (CLI describe) lists this gap: its --publish cannot complete a draft. This PR is built on origin/main, not on #220.

Format

One JSON object (UTF-8, two-space indent, sorted keys, LF), every key required:
format (morphobricks-description-draft), version (1), base_sha256, spec_commit (equal to tests/spec_fixtures/PIN.toml, tested), and six tables of answers only, keyed by the draft's stable keys (source folder labels, the repeat pairing label):

  • conditions: folder -> {id?, display_name?, reference?, target?, treatment_key?}
  • technical_repeats: folder (single for none) -> ID
  • repeats: pairing label -> {id?, confirmed_on?, display_name?} (confirmed when confirmed_on is present; that day is the caller's, §4.4a)
  • units: condition folder -> source repeat -> technical folder -> {channels, image_type?, display_name?}
  • channels: ID -> {display_name?, header_names?}
  • masks: ID -> {source_type, representation, source_channels?, display_name?}

Folders, counts and proposals are not stored: the draft is derived again from discovery (or the legacy design) on each resume. save_draft applies the answers to the draft and reads the exact bytes back with the strict reader first, so a file that resume would refuse is never written.

Regression evidence

New tests: tests/test_draft_format.py, tests/test_draft_file.py, tests/test_draft_answers.py (108 tests, helpers in tests/draft_file_support.py): round trip and deterministic bytes, 36 strict refusals (non-UTF-8, BOM, duplicate key, NaN, wrong format, unknown versions 2/"1"/true/1.0/missing, extra keys at four levels, missing keys, wrong types, bad dates, missing and malformed base), symlink, base mismatch (changed, replaced, gone), incomplete draft refused with every missing item listed, complete draft -> description -> publish_description path -> draft deleted, refusals (incomplete, reader refusal, changed base, held lock) leaving draft and published file byte-identical, legacy confirmation carried and not asked again, date is the user's and never the clock.
On origin/main code (new source absent): 3 collection errors, 0 of the 108 tests run (ModuleNotFoundError: No module named 'morphobricks_preprocessing.draft_answers', ImportError: cannot import name 'DRAFT_FILENAME'). With the change: 108 passed.
A real defect found by the tests while writing: used_ids.channels was empty in the built description; the strict reader refused it (E_EMPTY); fixed (declared channels are in used_ids).

Checks

python tools/check.py: Ruff and format clean (323 files); pytest 4454 passed, 4 skipped, 0 failed; code rules pass (new modules have layers in pyproject.toml, files under 600 lines, no baseline grew). The 4 skips are not in this change (3 case-insensitive-name tests, 1 file-mode test run as root).

Will conflict on CHANGELOG with other open PRs; keep both sides.

Needs an owner decision

  1. Seeding a draft from a published file. Draft.confirm_id refuses an ID already in used_ids, so a draft over a published base can only carry the published IDs as answers if used_ids passed to the draft holds the retired IDs only (this is what description_from_draft assumes: used_ids = retired IDs + every ID confirmed now). How a reconciliation (reconcile) result becomes a draft file (answers copied from the published file, confirmed_on kept) is not built here.
  2. A different spec_commit on resume. It is recorded and read strictly (40-digit hex) but not compared: refusing a draft written under another Spec commit would discard the user's answers, accepting it may apply other rules. Decide whether a mismatch refuses, warns, or is only recorded.
  3. Undeclared mask types. A discovered mask type with no masks answer is not a missing confirmation here (the Spec makes masks optional and the draft does not know the discovered mask types). Decide whether an undeclared mask type blocks publication in the notebook.
  4. Missing technical repeats of one condition in one repeat. A repeat that holds two folders of one condition raises DraftError (repeat_group_conflict) at build time; there is no pairing API yet to create one, so this only guards a future one.
  5. Pairing answers. Re-pairing differently named repeat folders is not a draft-file answer yet (the repeat key is the draft's pairing label). It belongs to the card 4 PR.
  6. A failed draft deletion after a successful publication raises and leaves the publication in place (undoing a publication for that would be worse); the stale draft is then refused on resume (base changed). Confirm this is the wanted behaviour.
  7. No plan §12 question is added (question 77 is taken on main); say if these decisions should be recorded as one.

Generated by Claude Code

Add morphobricks.draft.json, the versioned, strictly read file holding the
user's answers so far with base_sha256 and the pinned Spec commit, saved
atomically under the output lock, and the pure step that builds a Description
from a fully confirmed draft (refusing with every missing confirmation).
publish_draft publishes and then deletes the draft under one hold of the lock;
any refusal writes nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Scma6W3LC11s3qjXHZuGDm
@guijacquemet
guijacquemet merged commit 165ced0 into main Nov 8, 2026
7 checks passed
@guijacquemet
guijacquemet deleted the claude/description-draft-file branch November 8, 2026 05:02
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.

2 participants