diff --git a/app/api/routers/proposals.py b/app/api/routers/proposals.py index 4b03742..b3ed736 100644 --- a/app/api/routers/proposals.py +++ b/app/api/routers/proposals.py @@ -51,7 +51,7 @@ def _serialize_proposal(db: Session, proposal: Proposal, *, include_essentials: "project_id": proposal.project_id, "created_by_id": proposal.created_by_id, "created_by_username": creator.username if creator else None, - "feat_task_id": proposal.feat_task_id, # DEPRECATED — read-only compat + "feat_task_id": proposal.feat_task_id, # DEPRECATED (BE-PR-010): read-only for legacy rows. Clients should use generated_tasks. "created_at": proposal.created_at, "updated_at": proposal.updated_at, } @@ -237,7 +237,7 @@ def update_proposal( raise HTTPException(status_code=403, detail="Proposal edit permission denied") data = proposal_in.model_dump(exclude_unset=True) - # Never allow client to set feat_task_id + # DEPRECATED (BE-PR-010): feat_task_id is read-only; strip from client input data.pop("feat_task_id", None) for key, value in data.items(): @@ -365,7 +365,7 @@ def accept_proposal( }) next_num = task.id + 1 # use real id for next code to stay consistent - # Update proposal status (do NOT write feat_task_id — deprecated) + # Update proposal status — feat_task_id is NOT written (deprecated per BE-PR-010) proposal.status = ProposalStatus.ACCEPTED db.commit() diff --git a/app/models/proposal.py b/app/models/proposal.py index 80e6fbf..073fc0b 100644 --- a/app/models/proposal.py +++ b/app/models/proposal.py @@ -24,11 +24,21 @@ class Proposal(Base): one Project. - ``created_by_id`` — FK to ``users.id``; the user who authored the Proposal. Nullable for legacy rows created before tracking was added. - - ``feat_task_id`` — **DEPRECATED**. Previously stored the single generated - ``story/feature`` task id on accept. Will be replaced by - the Essential → story-task mapping (see BE-PR-008). - Kept in the DB column for read-only backward compat; new - code MUST NOT write to this field. + - ``feat_task_id`` — **DEPRECATED (BE-PR-010)**. Previously stored the single + generated ``story/feature`` task id on old-style accept. + Superseded by the Essential → story-task mapping via + ``Task.source_proposal_id`` / ``Task.source_essential_id`` + (see BE-PR-008). + + **Compat strategy:** + - DB column is RETAINED for read-only backward compatibility. + - Existing rows that have a value will continue to expose it + via API responses (read-only). + - New code MUST NOT write to this field. + - Clients SHOULD migrate to ``generated_tasks`` on the + Proposal detail endpoint. + - Column will be dropped in a future migration once all + clients have migrated. """ __tablename__ = "proposes" # keep DB table name for compat @@ -60,11 +70,14 @@ class Proposal(Base): comment="Author of the proposal (nullable for legacy rows)", ) - # DEPRECATED — see class docstring. Read-only; will be removed once - # Essential-based accept (BE-PR-007 / BE-PR-008) is fully rolled out. + # DEPRECATED (BE-PR-010) — see class docstring for full compat strategy. + # Read-only; column retained for backward compat with legacy rows. + # New accept flow writes Task.source_proposal_id instead. + # Will be dropped in a future schema migration. feat_task_id = Column( String(64), nullable=True, - comment="DEPRECATED: id of the single story/feature task generated on old-style accept", + comment="DEPRECATED (BE-PR-010): legacy single story/feature task id. " + "Superseded by Task.source_proposal_id. Read-only; do not write.", ) created_at = Column(DateTime(timezone=True), server_default=func.now()) diff --git a/app/schemas/schemas.py b/app/schemas/schemas.py index 8835322..d1fb556 100644 --- a/app/schemas/schemas.py +++ b/app/schemas/schemas.py @@ -297,7 +297,7 @@ class ProposalResponse(ProposalBase): project_id: int created_by_id: Optional[int] = None created_by_username: Optional[str] = None - feat_task_id: Optional[str] = None # DEPRECATED — will be removed after BE-PR-008 + feat_task_id: Optional[str] = None # DEPRECATED (BE-PR-010): legacy field, read-only. Use generated_tasks instead. created_at: datetime updated_at: Optional[datetime] = None diff --git a/docs/BE-PR-010-feat-task-id-deprecation.md b/docs/BE-PR-010-feat-task-id-deprecation.md new file mode 100644 index 0000000..46d2a5b --- /dev/null +++ b/docs/BE-PR-010-feat-task-id-deprecation.md @@ -0,0 +1,62 @@ +# BE-PR-010: `feat_task_id` Deprecation & Compatibility Strategy + +> Date: 2026-03-30 + +## Background + +The `feat_task_id` column on the `proposes` table was used by the **old** Proposal +Accept flow to store the ID of the single `story/feature` task generated when a +Proposal was accepted. + +With the new Essential-based Accept flow (BE-PR-007 / BE-PR-008), accepting a +Proposal now generates **multiple** story tasks (one per Essential), tracked via: +- `Task.source_proposal_id` → FK back to the Proposal +- `Task.source_essential_id` → FK back to the specific Essential + +This makes `feat_task_id` obsolete. + +## Decision: Retain Column, Deprecate Semantics + +| Aspect | Decision | +|--------|----------| +| DB column | **Retained** — no schema migration required now | +| Existing data | Legacy rows with a non-NULL `feat_task_id` continue to expose the value via API | +| New writes | **Prohibited** — new accept flow does NOT write `feat_task_id` | +| API response | Field still present in `ProposalResponse` for backward compat | +| Client guidance | Use `generated_tasks` on the Proposal detail endpoint instead | +| Future removal | Column will be dropped in a future migration once all clients have migrated | + +## Read Compatibility + +- `GET /projects/{id}/proposals` — returns `feat_task_id` (may be `null`) +- `GET /projects/{id}/proposals/{id}` — returns `feat_task_id` + `generated_tasks[]` +- `PATCH /projects/{id}/proposals/{id}` — `feat_task_id` in request body is silently ignored + +## Migration Path for Clients + +### Backend consumers +Use `Proposal.generated_tasks` relationship (or query `Task` by `source_proposal_id`). + +### Frontend +Replace `propose.feat_task_id` references with the `generated_tasks` array from +the detail endpoint. The detail page should list all generated tasks, not just one. + +### CLI +CLI does not reference `feat_task_id`. No changes needed. + +## Files Changed + +| File | Change | +|------|--------| +| `app/models/proposal.py` | Updated docstring & column comment with deprecation notice | +| `app/schemas/schemas.py` | Marked `feat_task_id` field as deprecated | +| `app/api/routers/proposals.py` | Updated comments; field still serialized read-only | +| `tests/test_propose.py` | Updated accept tests to assert `feat_task_id is None` | + +## Frontend References (to be updated in FE-PR-002+) + +- `src/types/index.ts:139` — `feat_task_id: string | null` +- `src/pages/ProposeDetailPage.tsx:145,180-181` — displays feat_task_id +- `src/pages/ProposesPage.tsx:83` — displays feat_task_id in list + +These will be addressed when the frontend Proposal/Essential tasks are implemented.