feature: Refinements become versioned specs refining another spec #13

Merged
Gilles merged 2 commits from feature/refinement-specs into master 2026-10-25 14:51:09 +00:00
Owner

Closes #12

Backend half of the refinement-as-spec refactor. Do not merge before the matching frontend PR — /api/refinements/ disappears and the spec/version payloads change.

Model

  • Refinement removed. A refinement is a Spec at a direct child place with refines → the refined spec (required by the API outside the root place; the refined spec's current version must be a draft, as before).
  • SpecVersion.refines_versions (M2M): versions of spec.refines this version covers. Monotonic (cover() / check_coverage()): never older than what the previous version covers; equal is allowed.
  • SpecVersion.is_absence: documented absence, content = justification (required to validate). Vacuously implemented, needs no refinements below.
  • SpecVersion.refinement_versions() / missing_refinement_places(): for each spec refining this one, its latest version covering it; completeness = ≥ 1 per direct child place (1..n allowed).
  • SpecCodeAlias: former -R- codes, resolved by the coverage upload after spec codes. Project.next_refinement_sequence removed.

Reopen cascade

  • Spec.reopen() cascades to refinement specs whose current version covers the reopened version → new drafts with pending_decision, covering nothing yet. A refinement already in draft just covers the new version.
  • Reopening a refinement directly keeps covering the parent version its previous version covered.
  • POST /api/spec-versions/{id}/decide/ {decision: keep|new_version|absence, reason} — top down (parent decided first). keep / absence revert the still-pending drafts below.
  • Pending drafts can't be edited or validated; project validation lists them.

API

  • SpecSerializer: refines, code_aliases, write-only is_absence + content on creation; place/refines fixed after creation. ?refines= filter.
  • SpecVersionSerializer: is_absence (writable on drafts), refines_versions, pending_decision, refinement_versions, missing_refinement_places.
  • Attachments and automatic tests only target spec versions / specs; exists_in_refinements removed from stored files.

Data migration (0007 → 0008 → 0009)

  • Target refinements: target spec refines the parent spec, target version covers the parent version, R-* → alias, attachments moved to the target version (same file deduplicated, name clash suffixed with the R code), automatic tests moved to the target spec (deduplicated, most recent run kept).
  • Absences grouped by (parent spec, child place) → one spec "Absence de raffinement : ", one version per distinct reason, status/validation copied from the parent version.
  • Stops (rolled back) if a spec is the target of two different parent specs, listing them — needs a read of prod data before deploying.
  • Covered by apps/specs/test_migrations.py.

Tests

pytest: 117 passed. pylint 10.00, autopep8 clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_014BSmBtGWeFBD9z4mYm9SjA

Closes #12 Backend half of the refinement-as-spec refactor. **Do not merge before the matching frontend PR** — `/api/refinements/` disappears and the spec/version payloads change. ## Model - `Refinement` removed. A refinement is a `Spec` at a direct child place with `refines` → the refined spec (required by the API outside the root place; the refined spec's current version must be a draft, as before). - `SpecVersion.refines_versions` (M2M): versions of `spec.refines` this version covers. Monotonic (`cover()` / `check_coverage()`): never older than what the previous version covers; equal is allowed. - `SpecVersion.is_absence`: documented absence, `content` = justification (required to validate). Vacuously implemented, needs no refinements below. - `SpecVersion.refinement_versions()` / `missing_refinement_places()`: for each spec refining this one, its latest version covering it; completeness = ≥ 1 per direct child place (1..n allowed). - `SpecCodeAlias`: former `-R-` codes, resolved by the coverage upload after spec codes. `Project.next_refinement_sequence` removed. ## Reopen cascade - `Spec.reopen()` cascades to refinement specs whose current version covers the reopened version → new drafts with `pending_decision`, covering nothing yet. A refinement already in draft just covers the new version. - Reopening a refinement directly keeps covering the parent version its previous version covered. - `POST /api/spec-versions/{id}/decide/` `{decision: keep|new_version|absence, reason}` — top down (parent decided first). `keep` / `absence` revert the still-pending drafts below. - Pending drafts can't be edited or validated; project validation lists them. ## API - `SpecSerializer`: `refines`, `code_aliases`, write-only `is_absence` + `content` on creation; `place`/`refines` fixed after creation. `?refines=` filter. - `SpecVersionSerializer`: `is_absence` (writable on drafts), `refines_versions`, `pending_decision`, `refinement_versions`, `missing_refinement_places`. - Attachments and automatic tests only target spec versions / specs; `exists_in_refinements` removed from stored files. ## Data migration (0007 → 0008 → 0009) - Target refinements: target spec `refines` the parent spec, target version covers the parent version, `R-*` → alias, attachments moved to the target version (same file deduplicated, name clash suffixed with the R code), automatic tests moved to the target spec (deduplicated, most recent run kept). - Absences grouped by (parent spec, child place) → one spec "Absence de raffinement : <parent>", one version per distinct reason, status/validation copied from the parent version. - **Stops** (rolled back) if a spec is the target of two different parent specs, listing them — needs a read of prod data before deploying. - Covered by `apps/specs/test_migrations.py`. ## Tests `pytest`: 117 passed. pylint 10.00, autopep8 clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_014BSmBtGWeFBD9z4mYm9SjA
Refinements become versioned specs refining another spec
All checks were successful
ci / test (pull_request) Successful in 2m8s
ci / e2e (pull_request) Has been skipped
ci / docker (pull_request) Has been skipped
ci / deploy (pull_request) Has been skipped
58bd199083
Replace the Refinement link model: a refinement is now a regular Spec at
a direct child place, with `Spec.refines` pointing at the refined spec.

- SpecVersion.refines_versions (M2M, monotonic): one refinement version
  can cover several versions of the refined spec.
- SpecVersion.is_absence: a documented absence is a refinement version,
  its content being the justification.
- Reopen cascades down the refinement tree as drafts with a pending
  decision (keep / new version / absence), settled top down through
  POST /spec-versions/{id}/decide/; a project version cannot be
  validated while a decision is pending.
- Former "-R-" codes survive as SpecCodeAlias, still resolved by the
  coverage upload; no new refinement code is issued.
- Data migration 0008 moves every Refinement row (targets, grouped
  absences, attachments, automatic tests) and stops on a target refined
  by two parent specs.
- /api/refinements/ and attachment/test refinement targets are removed.

Refs #12

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014BSmBtGWeFBD9z4mYm9SjA
Author
Owner
<!-- argus-link-bot:thierryade/backend --> 📊 [Rapports Argus pour cette PR](https://reports.stationmyr.net/thierryade/backend/pr/13)
Copy a spec refined from two parent specs instead of stopping
All checks were successful
ci / test (pull_request) Successful in 2m15s
ci / e2e (pull_request) Has been skipped
ci / docker (pull_request) Has been skipped
ci / deploy (pull_request) Has been skipped
332e60a6bd
Migration 0008 used to abort on a target spec refined from two different
parent specs (prod has one: FLO-3 <- FLO-2, FLO-18). The target now keeps
the parent of its oldest refinement; every other parent gets a copy of
it (versions, manual tests, attachments) under a new code, which its
refinements target and whose aliases they become.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014BSmBtGWeFBD9z4mYm9SjA
Author
Owner

Migration 0008 no longer stops on a spec refined from two parent specs (prod pre-check found one: FLO-3 <- FLO-2, FLO-18). The target keeps the parent of its oldest refinement; each other parent gets a copy (new code, title suffixed with the parent code, versions + manual tests + attachments copied, automatic tests left on the original). The copy has no refinements below it yet, so it will show missing child places until handled.

🤖 Generated with Claude Code

https://claude.ai/code/session_014BSmBtGWeFBD9z4mYm9SjA

Migration 0008 no longer stops on a spec refined from two parent specs (prod pre-check found one: FLO-3 <- FLO-2, FLO-18). The target keeps the parent of its oldest refinement; each other parent gets a copy (new code, title suffixed with the parent code, versions + manual tests + attachments copied, automatic tests left on the original). The copy has no refinements below it yet, so it will show missing child places until handled. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_014BSmBtGWeFBD9z4mYm9SjA
Gilles changed title from WIP: Refinements become versioned specs refining another spec to feature: Refinements become versioned specs refining another spec 2026-10-25 14:49:17 +00:00
Gilles merged commit 3833deca3f into master 2026-10-25 14:51:09 +00:00
Gilles deleted branch feature/refinement-specs 2026-10-25 14:51:09 +00:00
Sign in to join this conversation.
No description provided.