Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Chunk Contract: WS-ARCH-001-02A — TASK Submission Context Public API

Implementation state: in review; on merge, this chunk is complete and
WS-ARCH-001-02B becomes the next eligible implementation boundary.

## Parent initiative

WS-ARCH-001 — Modular Monolith Boundaries
Expand Down Expand Up @@ -45,10 +48,12 @@ backend/app/modules/tasks/service.py
backend/tests/architecture/test_module_boundaries.py
backend/tests/test_tasks.py
backend/tests/test_submission_bundle_admission.py
backend/scripts/behavior_ownership.py
.ci/module-boundaries/private-edge-debt.v1.json
.ci/behavior-ownership/**
.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-02A-task-submission-context-api.md
.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/evidence/WS-ARCH-001-02A-task-manifest.md
.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/reviews/WS-ARCH-001-02A-external-review-response.md
docs/architecture_lockdown.md
```

Expand All @@ -73,7 +78,7 @@ mutable dictionaries in the public API; new private edges.

```bash
(cd backend && .venv/bin/python -m ruff check app/modules/tasks tests/architecture/test_module_boundaries.py tests/test_tasks.py)
(cd backend && PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 .venv/bin/python -m pytest -q -p pytest_asyncio.plugin -p pytest_cov.plugin tests/architecture/test_module_boundaries.py tests/test_tasks.py tests/test_submission_bundle_admission.py --cov=app.modules.tasks.api --cov-report=term-missing --cov-fail-under=90)
(cd backend && export WORKSTREAM_TEST_DATABASE_URL="${WORKSTREAM_TEST_DATABASE_URL:?set WORKSTREAM_TEST_DATABASE_URL}" && PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 .venv/bin/python -m pytest -q -p pytest_asyncio.plugin -p pytest_cov.plugin tests/architecture/test_module_boundaries.py tests/test_tasks.py tests/test_submission_bundle_admission.py --cov=app.modules.tasks.api --cov-report=term-missing --cov-fail-under=90)
(cd backend && .venv/bin/python -m scripts.module_boundaries validate --protected-base origin/main)
python3 scripts/check_markdown_links.py
git diff --check
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# WS-ARCH-001-02A TASK Public Capability Manifest

## Public package

`backend/app/modules/tasks/api` exposes only dependency-safe immutable contracts:

- `TaskSubmissionContextRequest` — exact task, assignment, contributor, and
nullable predecessor selectors;
- `TaskLockedProjectContextReferences` — the immutable project, guide,
snapshot, effective-policy, and pre-submit-policy references copied onto the
TASK row; these are selectors for the future PROJECT capability, not PROJECT
policy bodies or canonical PROJECT facts;
- `SubmissionPredecessorFacts` — immediate immutable Submission ID and version;
- `TaskSubmissionContextFacts` — exact task/assignment/contributor status,
initial-or-revision kind, predecessor, and locked reference selectors;
- `TaskSubmissionContextPort` — transaction-bound lock/reload capability;
- `TaskSubmissionContextKind` — closed `initial | revision` context alias;
- `TaskSubmissionContextStatus` — closed `in_progress | needs_revision` status
alias;
- `TaskSubmissionContextFailure` — closed stable failure-code alias;
- `TaskSubmissionContextUnavailable` — stable bounded failure with
`task_submission_context_invalid` or
`task_submission_predecessor_changed`.

The public package imports no ORM model, repository, SQLAlchemy session,
PROJECT, CHECKER, ACTOR, ART, or mutable mapping.

## Owner-local implementation

`TaskRepository.lock_submission_context(...)` implements the structural port
inside TASKS. Its lock order is:

1. exact task;
2. exact assignment;
3. latest Submission for the task.

It validates active assignment ownership, assigned contributor, allowed task
state, complete locked reference selectors, the exact latest predecessor, and
the predecessor's contributor lineage.
No ACTOR, PROJECT, CHECKER, or ART row is read. An initial context requires no
existing Submission and a null predecessor selector. A revision context is
available only in `needs_revision` and requires the exact latest Submission ID,
version, and contributor; every crossed state fails closed.

## Boundary disposition

This chunk adds no cross-module edge and changes no live route, schema,
Submission persistence, authorization availability, or ART behavior. The
new eligible TASK public implementation is registered in the existing
behavior-ownership lifecycle partition and its additive-transition allowlist.
The legacy mixed `tasks.pre_submit_context` path remains frozen until 02B and 02C
provide the PROJECT and CHECKER public capabilities; 02D then migrates ART
consumers and removes the relevant private TASK edges. Unrelated frozen TASK
debt is unchanged.

This evidence describes the implementation branch only. On merge, 02A becomes
complete and 02B becomes the next eligible implementation chunk; the global
initiative ledger must not claim that state before the human merge decision.

## Deterministic proof

- public API boundary validation rejects private re-exports;
- focused initial, revision, stale-predecessor, and immutable-contract tests;
- PostgreSQL-backed initial/revision state-matrix coverage, including missing,
stale, crossed-state, and cross-contributor predecessor failures;
- a two-session PostgreSQL race proving concurrent context observation blocks
behind the canonical TASK-first lock;
- 100 percent focused coverage for `app.modules.tasks.api`;
- protected-base module-boundary validation;
- Ruff, Markdown link, and diff checks.

The dependency-free focused suite passes locally. The PostgreSQL cases are
part of the required hosted Backend/Agent Gates because this worktree has no
`WORKSTREAM_TEST_DATABASE_URL`; the chunk verification command fails fast when
that required database URL is absent.
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# WS-ARCH-001-02A External Review Response

## Comments addressed

- Derived runtime failure-code validation from the public Literal alias so the
type and runtime closed sets cannot drift.
- Made the TASK public-API import proof non-vacuous by requiring discovered API
files and discovered imports.
- Completed the changed repository method docstrings and documented the new
validation hooks and focused tests.
- Made the PostgreSQL lock-race cleanup cancel and await a pending contender
before closing either session.
- The earlier CodeRabbit CI warning is obsolete: every hosted backend gate and
shard passed at commit `a4104e47` before these review fixes.

## Comments deferred

None.

## Human decisions needed

None.

## Commands rerun

- Ruff over the changed TASK, boundary-test, and ownership-validator scope.
- Focused TASK public API and architecture tests with 100 percent coverage.
- Protected-base module-boundary validation, behavior-ownership validation,
Markdown-link validation, and diff integrity checks.

## Remaining risks

The PostgreSQL cancellation path and full backend shards must rerun in hosted
CI after this response commit. The local worktree intentionally has no test
database URL.
6 changes: 5 additions & 1 deletion .ci/behavior-ownership/partition.v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,10 @@
"group": "lifecycle",
"target": "backend/app/modules/reviews/schemas.py"
},
{
"group": "lifecycle",
"target": "backend/app/modules/tasks/api/submission_context.py"
},
{
"group": "lifecycle",
"target": "backend/app/modules/tasks/authorization.py"
Expand Down Expand Up @@ -761,7 +765,7 @@
"target": "backend/scripts/week2_api_e2e.py"
}
],
"authority_digest": "a5eda4a48b564b685080c6a387a3563c638b0e46bda0ac1b5b06d28f2f4b13ea",
"authority_digest": "b1e7516a8ceb1eccff47d177a665b5fe9c971b9cab9d420c828b7537f34e44b9",
"protected_base_commit": "7676ce4347db0c9694962a9b587a20765e16eac6",
"schema": "workstream.behavior-ownership-partition.v1"
}
25 changes: 25 additions & 0 deletions backend/app/modules/tasks/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""Dependency-safe public API for the TASKS business module."""

from app.modules.tasks.api.submission_context import (
SubmissionPredecessorFacts,
TaskLockedProjectContextReferences,
TaskSubmissionContextFacts,
TaskSubmissionContextFailure,
TaskSubmissionContextKind,
TaskSubmissionContextPort,
TaskSubmissionContextRequest,
TaskSubmissionContextStatus,
TaskSubmissionContextUnavailable,
)

__all__ = (
"SubmissionPredecessorFacts",
"TaskLockedProjectContextReferences",
"TaskSubmissionContextFacts",
"TaskSubmissionContextFailure",
"TaskSubmissionContextKind",
"TaskSubmissionContextPort",
"TaskSubmissionContextRequest",
"TaskSubmissionContextStatus",
"TaskSubmissionContextUnavailable",
)
111 changes: 111 additions & 0 deletions backend/app/modules/tasks/api/submission_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
"""Public TASK contracts for locked Submission lifecycle context."""

from __future__ import annotations

from dataclasses import dataclass
from typing import Literal, Protocol, get_args
from uuid import UUID

TaskSubmissionContextKind = Literal["initial", "revision"]
TaskSubmissionContextStatus = Literal["in_progress", "needs_revision"]
TaskSubmissionContextFailure = Literal[
"task_submission_context_invalid",
"task_submission_predecessor_changed",
]


class TaskSubmissionContextUnavailable(RuntimeError):
"""Report one stable failure without exposing TASK persistence."""

def __init__(self, code: TaskSubmissionContextFailure) -> None:
"""Validate and retain one failure from the public closed set."""
if code not in get_args(TaskSubmissionContextFailure):
raise ValueError("task submission context failure code is invalid")
super().__init__(code)
self.code = code


@dataclass(frozen=True, slots=True)
class TaskSubmissionContextRequest:
"""Exact TASK-owned selectors supplied to a transaction-bound port."""

task_id: UUID
assignment_id: UUID
contributor_id: UUID
predecessor_submission_id: UUID | None


@dataclass(frozen=True, slots=True)
class TaskLockedProjectContextReferences:
"""Immutable project-policy references locked onto one TASK row."""

project_id: UUID
guide_version: str
source_snapshot_id: UUID
source_snapshot_hash: str
effective_policy_id: UUID
effective_policy_hash: str
pre_submit_policy_id: UUID
pre_submit_policy_bundle_hash: str

def __post_init__(self) -> None:
"""Reject incomplete locked string references."""
values = (
self.guide_version,
self.source_snapshot_hash,
self.effective_policy_hash,
self.pre_submit_policy_bundle_hash,
)
if any(not value.strip() for value in values):
raise ValueError("task locked project context reference is empty")


@dataclass(frozen=True, slots=True)
class SubmissionPredecessorFacts:
"""TASK-owned identity of the immediate immutable Submission predecessor."""

submission_id: UUID
version: int

def __post_init__(self) -> None:
"""Reject non-positive or non-integer Submission versions."""
if type(self.version) is not int or self.version < 1:
raise ValueError("submission predecessor version is invalid")


@dataclass(frozen=True, slots=True)
class TaskSubmissionContextFacts:
"""Locked TASK, assignment, predecessor, and project-reference facts."""

task_id: UUID
assignment_id: UUID
contributor_id: UUID
status: TaskSubmissionContextStatus
kind: TaskSubmissionContextKind
predecessor: SubmissionPredecessorFacts | None
locked_project_context: TaskLockedProjectContextReferences

def __post_init__(self) -> None:
"""Enforce the exact initial-or-revision lifecycle shape."""
is_initial = (
self.status == "in_progress"
and self.kind == "initial"
and self.predecessor is None
)
is_revision = (
self.status == "needs_revision"
and self.kind == "revision"
and self.predecessor is not None
)
if not (is_initial or is_revision):
raise ValueError("task submission context predecessor is inconsistent")


class TaskSubmissionContextPort(Protocol):
"""Transaction-bound TASK capability implemented inside the TASK module."""

async def lock_submission_context(
self,
request: TaskSubmissionContextRequest,
) -> TaskSubmissionContextFacts:
"""Lock and return exact TASK-owned lifecycle facts."""
Loading
Loading