docs: add concept and V1 implementation plan

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Alec K2XAP 2026-10-26 22:27:06 -04:00
commit 82bcdde043
2 changed files with 271 additions and 0 deletions

69
docs/concept.md Normal file
View file

@ -0,0 +1,69 @@
Intent is an app for defining high level feature sets for projects that span one or more implementations.
It's "GitHub for intent". Where tools like GitHub and Forgejo are for tracking the _how_, this tool tracks the _why_. Instead of files of code being the unit of collaboration, it's features. Users can create an issue that describes a problem or new desired functionality, discuss, and then create a new feature or link to an existing feature, with the necessary changes described. The history of the feature is tracked, so the decisions behind why something works the way it does can be known.
This then feeds into the implementations, where one or more repositories of app or package implementations can be linked, and their compliance with the feature set tracked and organized.
Data model:
Organization - higher level wrapper around multiple projects, collaborators
Project - root set of logically-associated problems
Issue - a pre-proposal, bug report, etc that can optionally be linked to an existing Feature, or used to create a new Feature (promoted to a Feature(status=active)), or not associated with any Feature, (status=open, rejected, accepted), a potential change to a feature
Feature - a free markdown description of a particular functional solution to a problem within a Project, with discussion (comments can be resolved, or converted to Issues), associated Issues, prototype files, documentation files, and associated Implementation status. Can have parent Features to create a hierarchy of any depth (and have independent statuses for V1). Have a stable ID to be referenced by Implementation changes and Issues, and a status. They also have a sequential revision number that Implementations can reference to catch up with the latest version of the Feature. (status=active, withdrawn, superseded)
Implementation - name and status (active, archived), a repository in any forgejo instance, one or more can be linked to the project and represent a particular implementation of the project, they have features associated, not necessarily all features (marked N/A for that implementation). Intent itself only links to a repo host elsewhere, it does not need to store the repo itself. It can provide links to implementation releases (V1.1). Each Implementation can be on a different Forgejo instance (assume the Forgejo already has the Intent instance onboarded once we get to V1.1, to start it's purely a URL to click on). Per feature × implementation status is one of: n/a, not started, in progress, shipped, retired, stamped with the feature revision number it applies to so it's clear if an Implementation has fallen behind.
CommentThread - hangs off specific lines in the Feature description, the general feature, a whole file, a specific coordinate of an image file, an Issue. Can be resolved (to collapse and hide)
User - basic user account, optionally linked to Organization as Team Member
Comment - a specific versioned comment from a User that's part of a CommentThread
Feature history is like git history, each edit is a commit with notes explaining the change. They are edited directly by team members (as if commiting directly to main) - and edits can be tied to an Issue. To start, Implementation status is purely manual. The Intent data lives entirely within its own data store.
For V1.1: after making changes to a feature, the user can optionally create issues back on the forgejo instance to do the implementation (optionally passing agent-specific instructions on top of links back to the Intent content). (Flow is mostly one-way, Intent -> Forgejo, but delivery status does propagate back to Intent via commit messages. Issues on Forgejo do not propagate back to Intent.)
Users can authenticate using Forgejo, GitHub, Discord, or email (magic link) (and multiple of these by linking their auths together). System admin users can use password authentication (these users are separate from regular users and cannot act within projects).
Tenancy: multiple organizations and teams, potentially many public users
(just a few organizations to start and only a handful of projects and colalborators, at most dozens of users overall)
Users of the Intent app DO NOT need accounts with the underlying Forgejo instances. (V1.1: only users with Forgejo access can set up the syncing with the target repositories etc, via a Forgejo service user + scoped token stored per org. All users can see the Implementation status, but the visibility of the Forgejo project's details propagates to Intent.) In V1 an Implementation is just a URL and Intent never calls the Forgejo API.
Not needed for V1: notifications, search, public API or MCP, Forgejo issue creation, release links
Organization and user structure imitates forgejo (does not sync data but a user linking a repo from forgejo requires an account on both, such as a service-user):
- Organization (can only be created by sysadmin for V1, requires specifying an owner User who can add additional team members, only owner-role can manage team members though they need to accept invite and can remove themselves)
- team members (Users who are either collaborators or owners of the org): one set of team members per org
- Project
- any team member with the org-level project-create grant (set by org owners) can create a project and becomes its owner
- team member can have project-owner permissions (admin actions like delete project, manage project members) and/or collaborator actions (accept issues, create and update features, create/link implementations and update statuses). Collaborator access is granted per project.
- org owners can add themselves to any org project as collaborators, and admin the project
- can allow open contribution from other non-team Users (Issue creation, comments) or restrict these to team members only
- may be completely private and only visible to project members (plus org owners), require auth so only logged-in users can see, or fully public so the world can see and browse (but only logged-in users can contribute/comment)
- Users
- can freely contribute to projects that allow open contributions (depending on those permissions) including comment on features and promote those comment threads to an Issue (original commenter or team member can do this, other users cannot)
- cannot create projects outside of an organization
Front-end approach: Django + HTMX + Alpine to start with islands of rich interactivity where needed.
Styling: extremely minimal. Largely browser defaults; if the stylesheet failed to load the look-and-feel should not be drastically different (layout aside). Accessibility is key. Dark mode from the start.
Back-end: Django, Sqlite (Litestream), deployed onto Fly.io and using Tigris object store. Database backups to B2
Local development: Docker (Fly uses Docker also)
Transactional email (magic links, invites): provider TBD. Console backend in development.
Already have credentials for Fly and B2.
See plan.md for the V1 implementation plan and the defaults chosen for details not covered here.
For example:
1. A general User creates a new Issue in a Project.
2. Multiple Users including team members discuss in comments on the Issue.
3. A Team Member decides to accept the Issue, which closes it to status=accepted, and the creates a new Feature and writes the description of it (the Feature is linked to the Issue it came from)
4. The Team Member then updates the status of the Feature manually to track with implementations
5. Later, a User starts a comment thread on a Feature.
6. After some Discussion, the User promotes the thread to an Issue.
7. More discussion, then a Team Member accepts the Issue and updates the Feature (manually for V1)
- or, a Team Member rejects the Issue and it's marked as rejected, but still linked to the Feature so others can see it was put forward and rejected

202
docs/plan.md Normal file
View file

@ -0,0 +1,202 @@
# 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. Email invites create a pending invite, accepted via a signed link that also logs the user in (magic link). Expire after 14 days. The last org owner cannot be removed or demoted. |
### 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 configured per Forgejo instance (Forgejo supports OIDC), email login-by-code for magic links. Multiple social accounts per user via allauth's connect flow. |
| 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 `<iframe>` whose `src` is a signed URL on the Tigris origin, never rendered inline on the app origin. |
| Markdown | `markdown-it-py` with source line mapping (`data-line` attributes on blocks) for line threads; output sanitized with `nh3`. Diffs via `difflib` rendered server-side. |
| Frontend | Django templates, HTMX for partial updates, Alpine for small local state (image pin placement, thread forms). CodeMirror 6 as a progressive enhancement on the markdown textarea, optional. No build step beyond copying vendored JS into static files. |
| Email | `django-anymail` with the provider pluggable; console backend in development. **Provider TBD; launch blocker for magic links and email invites.** |
| Background work | None. Email is sent synchronously. |
| Observability | Sentry (free tier), Fly logs. |
| Tests | `pytest-django` for models, permissions and views; a handful of Playwright smoke tests for HTMX flows. |
| Local dev | Docker Compose: one app container (Django runserver), SQLite in a volume, MinIO as a stand-in for Tigris. `.env` for secrets. |
| Deploy | Fly.io, one machine, one volume, `release_command` runs migrations, Litestream wraps the app process. CI assumed to be Forgejo Actions in the Intent repo: lint, test, `fly deploy` on main. |
### Styling and accessibility
The app should look close to unstyled HTML. Guiding rule: if the stylesheet fails to load, only layout suffers.
- Semantic HTML first: `<main>`, `<nav>`, `<article>`, `<section>`, `<h1>`–`<h6>` in order, `<button>` for actions, `<a>` for navigation, `<table>` for tabular data, `<details>` for collapsibles, `<dialog>` where a modal is needed.
- One small stylesheet (target under 5 KB). It sets: `color-scheme: light dark` on `:root`, a readable max line width, spacing, focus-visible outline, and a few utilities. Colors come from `light-dark()` or system colors; no custom palette beyond a link color and a muted text color.
- Dark mode via `prefers-color-scheme` from day one, with a three-way toggle (system/light/dark) stored in `localStorage` and applied via `data-theme` on `<html>`.
- Every HTMX interaction has a non-JS fallback: forms post normally, links navigate. HTMX enhances, never gates.
- Keyboard: all actions reachable by tab; skip link to `<main>`; visible focus. Image pins get a list alternative (threads listed below the image with coordinates). Line threads have a per-line "comment" button, not hover-only.
- Contrast and semantics checked with axe in the Playwright smoke tests.
## 3. Data model
Fields are indicative, not exhaustive. All models have a ULID `id` primary key plus `created_at`/`updated_at` via a common abstract base. Human-facing keys (`Organization.slug`, `Project.slug`, feature `AUTH-12`, issue `#N`) are separate columns used in URLs; the ULID appears in URLs only for models without one. Soft deletes only where noted.
```
User allauth user; no password for regular users; is_staff = sysadmin
Organization slug, name
OrgMembership org, user, role ∈ {owner, collaborator}, can_create_projects: bool
OrgInvite org, email | user, role, token, expires_at, accepted_at
Project org, slug, name, feature_prefix, visibility ∈ {private, authenticated, public},
open_contribution: bool, next_feature_number, next_issue_number
ProjectMembership project, user, role ∈ {owner, collaborator}
Feature project, number, parent (nullable, same project), title, status, superseded_by,
current_revision (int), created_by
FeatureRevision feature, revision, description_md, note, author, parent_at_revision,
status_at_revision, issues (M2M)
FeatureFile feature, kind ∈ {prototype, documentation}, name, current_version
FeatureFileVersion file, version, storage_key, content_type, size, created_in_revision
Implementation project, name, url, status ∈ {active, archived}
FeatureImplStatus feature, implementation, status, feature_revision, updated_by (unique feature+impl)
Issue project, number, title, body_md, author, status, duplicate_of,
features (M2M), promoted_from_thread (nullable)
CommentThread project, target: one of {feature, feature_line, feature_file, file_image_point, issue},
feature, issue, revision, line_start, line_end, file_version, x, y,
resolved_by, resolved_at, promoted_to (issue)
Comment thread, author, body_md, deleted_at
CommentEdit comment, body_md, edited_at
```
Notes:
- `FeatureRevision` is append-only. `Feature` holds denormalized current values for cheap listing.
- Feature number and issue number are allocated inside a transaction with `select_for_update` on the project row (fine under SQLite's single-writer model).
- A `CheckConstraint` on `OrgMembership` rejects `user.is_staff`. Enforced again in a model `clean()` for admin forms.
- Thread target is validated in `clean()`: exactly the fields required for its `target` type are set.
## 4. Permissions
Resolved per request into a small `Access` object (`can_view`, `can_comment`, `can_create_issue`, `can_edit_features`, `can_manage_impl`, `can_admin`) and passed to templates. One module, well tested, used everywhere.
| Action | Anonymous | Logged-in non-member | Org collaborator (no project grant) | Project collaborator | Project owner / org owner |
|---|---|---|---|---|---|
| View public project | ✓ | ✓ | ✓ | ✓ | ✓ |
| View authenticated project | | ✓ | ✓ | ✓ | ✓ |
| View private project | | | | ✓ | ✓ |
| Comment, create issue | | if open contribution | if open contribution | ✓ | ✓ |
| Promote own thread to issue | | if open contribution | if open contribution | ✓ | ✓ |
| Promote any thread to issue | | | | ✓ | ✓ |
| Accept/reject/reopen issue | | | | ✓ | ✓ |
| Create/edit feature, files, status | | | | ✓ | ✓ |
| Link implementations, set statuses | | | | ✓ | ✓ |
| Resolve threads, delete comments | own | own | own | ✓ | ✓ |
| Manage project members, delete project | | | | | ✓ |
| Create project | | | if `can_create_projects` | | org owner ✓ |
| Manage org members, grants | | | | | org owner |
| Create organization | | | | | sysadmin only |
Org owners are treated as project owners on every project in the org without needing a membership row.
## 5. Phases
Each phase ends deployable. Rough sizes assume one person, part-time; adjust freely.
### Phase 0 — Skeleton and deploy (small)
- Repo, `uv` project, Django 5.2, settings split (base/dev/prod), `ruff`, `pytest-django`.
- Abstract base model with ULID primary key and timestamps; all models inherit it.
- Docker Compose for local dev with MinIO.
- Base template: semantic layout, skip link, nav, the minimal stylesheet, dark mode toggle.
- Fly app, volume, Litestream to B2, `release_command` for migrations, Sentry.
- CI: lint + test + deploy on main.
- **Exit:** empty app with a health page running on Fly; restore from Litestream tested once.
### Phase 1 — Accounts and organizations (medium)
- allauth with GitHub, Discord, one Forgejo OIDC provider, email login-by-code (console backend).
- Account linking UI (allauth connections page, restyled minimally).
- Sysadmin: Django admin, create orgs with initial owner.
- Org pages: member list, roles, `can_create_projects`, invites by email/username, accept/decline, leave org, last-owner guard.
- Permissions module with tests.
- **Exit:** sysadmin creates an org; owner invites a user; user logs in with two linked providers.
### Phase 2 — Projects and features (large)
- Project create (prefix, visibility, open contribution), member management, delete.
- Feature CRUD as revisions: create, edit description with required note, change parent, change status, `superseded_by`.
- Revision history page, diff view between any two revisions, per-revision read-only view.
- Feature tree view (nested `<ul>`/`<details>`), flat list, feature page with children listed.
- Markdown rendering with line mapping and sanitization.
- **Exit:** full feature lifecycle works without JS enabled.
### Phase 3 — Issues and comments (large)
- Issue CRUD, statuses, duplicate linking, M2M to features, "accept and create feature" flow prefilling the feature from the issue.
- Comment threads on issues and on features (general), with resolve, edit history, tombstone delete.
- Line-anchored threads: gutter buttons per rendered block, thread list beside the description, outdated marking on newer revisions.
- Promote thread to issue.
- HTMX partials for posting comments and resolving threads, with form fallbacks.
- **Exit:** the example flow in concept.md (steps 1–7) works end to end.
### Phase 4 — Files and implementations (medium)
- File upload to Tigris, versions, signed URLs, sandboxed iframe for HTML, image display.
- Image-coordinate threads (Alpine for pin placement, list fallback).
- File-level threads.
- Implementations: CRUD, URL, status; per-feature status table on the implementation page and per-implementation status on the feature page; "behind" indicator against current revision.
- **Exit:** a feature with a prototype image has a pinned thread; an implementation shows one feature shipped and one behind.
### Phase 5 — Launch hardening (small)
- Pick email provider, configure anymail, verify magic link and invite emails.
- Moderation: deactivate user (sysadmin), delete comments (project owner), ghost user on account deletion.
- Playwright smoke tests with axe checks on the main pages in both color schemes.
- Rate limiting on login and comment endpoints (`django-ratelimit`), CSP header, security headers review.
- Backup restore drill repeated against the real volume.
- **Exit:** launch checklist (below) complete.
## 6. Launch checklist
- [ ] Email provider configured; magic link and invite emails tested in production
- [ ] Litestream restore verified from B2 into a fresh volume
- [ ] Fly volume snapshots enabled as a second backup
- [ ] Sentry receiving errors from production
- [ ] At least one Forgejo OIDC provider registered and tested
- [ ] axe passes on feature, issue, project and org pages, light and dark
- [ ] Keyboard-only walkthrough of the example flow
- [ ] Privacy check: private project content not reachable via file URLs, feeds, or admin leaks
## 7. Hooks for V1.1
Decisions made now so V1.1 does not need a migration redesign:
- `Implementation.url` is kept separate from future `forgejo_instance` and `repo_path` fields; the URL stays the display link.
- `FeatureImplStatus` already records `feature_revision`, which is what commit trailers will set (`Intent: AUTH-12@7`).
- Org-level `ForgejoCredential` (instance URL, service-user token) is the planned home for sync credentials; nothing in V1 depends on it.
- Feature stable IDs and revision numbers are never reused or renumbered, so external references stay valid.