BE-PR-010: deprecate feat_task_id — retain column, read-only compat
- Updated model docstring with full deprecation strategy - Updated column comment to mark as deprecated (BE-PR-010) - Updated schema/router comments for deprecation clarity - Added deprecation doc: docs/BE-PR-010-feat-task-id-deprecation.md - feat_task_id superseded by Task.source_proposal_id (BE-PR-008)
This commit is contained in:
@@ -51,7 +51,7 @@ def _serialize_proposal(db: Session, proposal: Proposal, *, include_essentials:
|
|||||||
"project_id": proposal.project_id,
|
"project_id": proposal.project_id,
|
||||||
"created_by_id": proposal.created_by_id,
|
"created_by_id": proposal.created_by_id,
|
||||||
"created_by_username": creator.username if creator else None,
|
"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,
|
"created_at": proposal.created_at,
|
||||||
"updated_at": proposal.updated_at,
|
"updated_at": proposal.updated_at,
|
||||||
}
|
}
|
||||||
@@ -237,7 +237,7 @@ def update_proposal(
|
|||||||
raise HTTPException(status_code=403, detail="Proposal edit permission denied")
|
raise HTTPException(status_code=403, detail="Proposal edit permission denied")
|
||||||
|
|
||||||
data = proposal_in.model_dump(exclude_unset=True)
|
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)
|
data.pop("feat_task_id", None)
|
||||||
|
|
||||||
for key, value in data.items():
|
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
|
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
|
proposal.status = ProposalStatus.ACCEPTED
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|||||||
@@ -24,11 +24,21 @@ class Proposal(Base):
|
|||||||
one Project.
|
one Project.
|
||||||
- ``created_by_id`` — FK to ``users.id``; the user who authored the Proposal.
|
- ``created_by_id`` — FK to ``users.id``; the user who authored the Proposal.
|
||||||
Nullable for legacy rows created before tracking was added.
|
Nullable for legacy rows created before tracking was added.
|
||||||
- ``feat_task_id`` — **DEPRECATED**. Previously stored the single generated
|
- ``feat_task_id`` — **DEPRECATED (BE-PR-010)**. Previously stored the single
|
||||||
``story/feature`` task id on accept. Will be replaced by
|
generated ``story/feature`` task id on old-style accept.
|
||||||
the Essential → story-task mapping (see BE-PR-008).
|
Superseded by the Essential → story-task mapping via
|
||||||
Kept in the DB column for read-only backward compat; new
|
``Task.source_proposal_id`` / ``Task.source_essential_id``
|
||||||
code MUST NOT write to this field.
|
(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
|
__tablename__ = "proposes" # keep DB table name for compat
|
||||||
@@ -60,11 +70,14 @@ class Proposal(Base):
|
|||||||
comment="Author of the proposal (nullable for legacy rows)",
|
comment="Author of the proposal (nullable for legacy rows)",
|
||||||
)
|
)
|
||||||
|
|
||||||
# DEPRECATED — see class docstring. Read-only; will be removed once
|
# DEPRECATED (BE-PR-010) — see class docstring for full compat strategy.
|
||||||
# Essential-based accept (BE-PR-007 / BE-PR-008) is fully rolled out.
|
# 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(
|
feat_task_id = Column(
|
||||||
String(64), nullable=True,
|
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())
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
|||||||
@@ -297,7 +297,7 @@ class ProposalResponse(ProposalBase):
|
|||||||
project_id: int
|
project_id: int
|
||||||
created_by_id: Optional[int] = None
|
created_by_id: Optional[int] = None
|
||||||
created_by_username: Optional[str] = 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
|
created_at: datetime
|
||||||
updated_at: Optional[datetime] = None
|
updated_at: Optional[datetime] = None
|
||||||
|
|
||||||
|
|||||||
62
docs/BE-PR-010-feat-task-id-deprecation.md
Normal file
62
docs/BE-PR-010-feat-task-id-deprecation.md
Normal file
@@ -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.
|
||||||
Reference in New Issue
Block a user