# Intent V1 implementation plan Companion to [concept.md](concept.md). The concept says what Intent is; this says how V1 gets built, in what order, and which details were decided by default rather than in the concept. Where the two disagree, fix the concept first. ## 1. V1 scope **In:** organizations, projects, features (markdown, revisions, hierarchy, files), issues, comment threads (feature, line, file, image coordinate, issue), implementations as plain URLs with per-feature manual status, auth via Forgejo/GitHub/Discord/magic link with account linking, sysadmin via Django admin, three project visibility levels, open contribution toggle, invites. **Out (V1.1 or later):** notifications, search, public API or MCP, Forgejo API access of any kind (issue creation, release links, visibility propagation, commit-trailer status), status inheritance through the feature hierarchy, real-time updates. ## 2. Decisions and defaults Decisions made in the concept are not repeated. These fill the gaps. ### Model | Topic | Default | |---|---| | Primary keys | Opaque, non-sequential: ULID stored as a 26-char string, generated in Python (`python-ulid`), set as `default` on a shared abstract base model. Never exposed as a sequence; used in URLs only where no human-readable key exists (files, file versions, threads, comments). | | Feature stable ID | Human-facing, separate from the PK: per-project prefix + sequence, e.g. `AUTH-12`. Prefix is set on project creation, uppercase, 2–8 chars, unique within the org. Number never reused. | | Issue ID | Per-project sequence `#N`, separate from feature numbers. | | Feature revision | Integer starting at 1, incremented by every commit to the feature: description edit, file add/replace/remove, parent change, status change. Each revision stores the full description text (not a diff); diffs are computed on display. | | Commit note | Required, short, free text. Optionally linked to one or more issues. | | Issue ↔ feature | Many-to-many. An issue promoted from a comment thread is linked to that thread's feature. Accepting an issue that creates a feature links both. | | Issue statuses | `open`, `accepted`, `rejected`, `withdrawn` (author closes), `duplicate` (links to another issue). Collaborators can reopen. | | Feature statuses | `active`, `withdrawn`, `superseded` (requires `superseded_by` feature). | | Implementation status | `active`, `archived`. | | Feature × implementation status | `n/a`, `not_started`, `in_progress`, `shipped`, `retired`; stamped with the feature revision it applies to. Missing row displays as `not_started @ —`. If stamped revision < current revision, display "behind". | | Hierarchy | Single optional parent within the same project. Cycles rejected. Moving a feature records a revision. Statuses independent in V1. | | Files | Belong to a feature, typed `prototype` or `documentation`. Replacing a file creates a new file version and a feature revision; old versions stay (so old image-pin threads still resolve). Size cap 25 MB; allowed types: images, PDF, markdown/text, HTML, zip. | | Line-anchored threads | Anchor = feature revision + start/end source line. On later revisions the thread shows as "outdated" with a link to the revision it was made on; no attempt to re-map lines. | | Image threads | Anchor = file version + x/y as fractions (0–1). | | Comments | Edits keep history (visible "edited" link). Author or project owner may delete; a deleted comment shows as a tombstone. Resolving a thread is allowed for the thread author or any collaborator. | | Thread → issue | Original commenter or any team member. Thread gets `promoted_to` issue; issue body is prefilled with the thread text and links back. | | Deleted users | Content remains, attributed to a "ghost" user. | | Moderation | Project owners can delete comments and close issues in their project. Sysadmin can deactivate a user site-wide. | | Invites | By email or existing username. Username invites appear on the invitee's Invitations page (no notifications in V1). Email invites send a link carrying a random token (only its hash is stored); the link requires sign-in and whoever holds it may accept with any account. Email invites also appear on the Invitations page of a user with that verified email. Re-inviting replaces the pending invite. Expire after 14 days. The last org owner cannot be removed, demoted or leave. | | URLs | Organizations live at the root: `//`, `///`, `///features/INT-12/`. App routes are reserved slugs at both levels. | | Project slug and prefix | Fixed after creation, since they appear in URLs and feature IDs. Name, visibility and open contribution stay editable. | | Hidden projects | A project the user can't see returns 404 (not 403), so private projects don't reveal they exist. Signed-out visitors are sent to sign in instead. | | Project owners | No last-owner guard: org owners can always administer every project. Leaving the org removes a user's project roles in it. | | Feature editing | One form edits title, description, parent, status and replacement, with a required change note. Saving with no changes is refused. A "Preview description" button renders the Markdown server-side, so preview works without JS. Django admin shows features read-only so every change goes through revisions. | ### Stack | Topic | Default | |---|---| | Runtime | Python 3.13, Django 5.2 LTS, `uv` for dependencies, `ruff` for lint/format. | | Auth | `django-allauth`: GitHub, Discord, OpenID Connect provider per Forgejo instance (Forgejo supports OIDC). Sysadmins register each provider as a SocialApp in Django admin, so adding a Forgejo instance needs no deploy. Email sign-in is allauth's login-by-code: the "magic link" is an emailed one-time code, not a clickable link. Signup asks for email and username only; regular users never get a password. Multiple social accounts per user via allauth's connect flow (no automatic linking by email). Usernames: letters, digits, `-`, `_`, max 39. | | Sysadmins | Django `is_staff` users with passwords, Django admin at `/admin/`. A model constraint rejects staff users as org members. Regular users have no password (unusable). | | Database | SQLite on a Fly volume. `transaction_mode = "IMMEDIATE"`, WAL mode, `busy_timeout` 5 s. Litestream replicates to B2 continuously. | | Files | Tigris via `django-storages` S3 backend, private bucket, time-limited signed URLs. HTML prototypes are shown in a sandboxed `