Web frontend and supporting services for lance.blue
1 folder · 43 files

README.md

Plan #

What lance.blue is building, and roughly in what order. One file per epic. milestones.md is the other view of the same work: releases worth naming, in prose, without reference to any of this.

An epic is a line of work that takes many pull requests and has an exit criterion somebody could check. It is not a task list — the tasks live inside it, and inside the TODOs of whichever repo does the work. Everything here spans the whole project, not just this repo: headquarters is the main repo, so an epic that is mostly arena's or infra's work still has its file here.

Two decisions shape all of it #

State is PDS-first. Durable, player-owned artifacts — match results, challenges, camo references — live as records in the player's own PDS, not in a database of ours. headquarters-api keeps only OAuth sessions and an ephemeral live-match registry. This is why so many epics carry a schema, and why managed-store is about server state rather than about everything.

Compose-first. headquarters-api, web and arena all run locally under Docker Compose before anything runs on AWS.

The id is the commit scope #

Each epic's filename is its id, and that id never changes. plan/complete/camo.md is camo, so its commits read feat(camo): … and fix(camo): …. That is the whole reason the ids have no number prefix, and the reason there is no order field either: a filename or a field that has to be rewritten when something else moves is not an identifier. Archiving moves the file between directories and never renames it, for the same reason.

plan is a valid scope too, for changes to this register.

Order is advisory, and unnumbered #

The Open table below reads roughly top to bottom, and that is the whole of the ordering. There is no order field and no numbered column, because a total order over twenty epics is a lie that costs something to maintain: inserting one at the front used to renumber every file beneath it and every row here, so two branches each adding an epic conflicted by construction, over a number neither of them cared about.

What actually constrains the work is dependsOn, which says what genuinely cannot start first, and status, which says what is blocked. Those are per epic, they are true independently of what anybody else is doing, and moving an epic is moving one line.

The sequence the tables read in comes from order.txt, which is a list of ids and nothing else. It is advisory and it is optional: an epic it does not name still appears, alphabetically, at the end of whichever table it belongs to. So a new epic needs no line in it — add one only when where the epic sits is worth saying to somebody reading down the list.

Status #

shipped — the exit criterion is met. The file stays, because how something was built and what it cost to learn is worth more after it works than before. A shipped epic with nothing open left moves to complete/; one with loose ends stays here until they close.

open — being worked on or ready to be.

blocked — cannot start until something in dependsOn lands.

continuous — no exit criterion and no completion date. Worked whenever adjacent code is open. Accessibility and copy are like this: they are never done, and pretending otherwise just produces an epic that is permanently 80% complete.

declined — decided against. Not blocked, which is waiting: the answer is no rather than not yet. The file stays, because a decision that is not written down gets made again.

The tables are generated #

The five tables below are output, not source. scripts/gen-plan-readme.py reads every epic's frontmatter and rewrites what sits between the <!-- generated: … --> fences. Everything around them is hand-written and is passed through untouched, so prose can go anywhere except inside a fence. The plan-register prek hook runs the script with --check, which means a row that disagrees with the file it points at fails the commit.

Adding an epic is therefore one file and one command: write plan/<id>.md with the frontmatter keys every other epic carries, run scripts/gen-plan-readme.py, and stage both. Nothing here is typed by hand — that is what stops two branches each adding an epic from conflicting over rows neither of them cared about.

Which table an epic lands in follows from the file as well. complete/ is Complete, and outside it the status picks between the other four. Shipping an epic, or archiving it, is an edit to the epic and nothing else.

Complete #

Nothing open, nothing left to decide. These live in complete/ so the list above stays the list of things somebody might work on. They are not deleted — how something was built, and what it cost to learn, is worth more after it works than before — and their ids stay valid commit scopes, because a follow-up fix to a finished epic is still that epic's commit.

id title
sign-in A player signs in with their own ATProto account
camo Player-owned camo, drawn here, stored in their PDS, painted in the match
match-startup A slow match start can be attributed instead of guessed at
match-launch A match runs on Fargate from a manifest we authored

Shipped, with loose ends #

The exit criterion is met and something is still open in the file.

id title
lobby Several humans get into one container
flare In-match feedback reaches a public board

Open #

id title status
match-records A finished match is player-owned, checkable data blocked
match-lifecycle A match starts, survives, and ends properly open
match-control In-match commands run through us, not MegaMek's chat box open
session-security A session is only as alive as the grant behind it open
managed-store Losing the box does not lose login grants open
proxy-split Deploying the API does not drop every live match open
security-review The boundaries have been looked at deliberately, not incidentally open
scenarios The scenario catalog is generated and validated, not curated open
user-safety A player has somewhere to go when another player is the problem open
invitations Nobody is put in a match without being asked open
player-profile A player has a page that is about them open
onboarding Somebody who has never played MegaMek can finish a match open
daily-challenge One fight a day, the same for everyone, scored open
cost-model What a match costs is measured, and what a busy day costs is known open
unit-library Every unit MegaMek knows is queryable, from any MegaMek open
unit-search Filtering units means the same thing everywhere it happens open
unit-rules A force's battle value is ours to compute, and the same on both sides blocked
forces A player brings their own list open
post-to-bluesky A player can post a match to Bluesky, and is never surprised by one blocked
social-presence Somebody who finds one post can find the project open
achievements A player is awarded something and chooses to show it open
appview lance.blue shows a match it never hosted open
leaderboard A rating anybody can recompute open
seasons A run of days is a season with a theme and an end blocked
after-action A finished match is worth reading, not just counting open
campaign Pilots and forces persist between matches open
unlocks A campaign is walked by unlocking the library one design at a time blocked
tournaments A tournament says what may enter, and checks it blocked
house-rules One named set of rules every lance.blue match is played under open
setup-tree Match setup is one tree, rendered as a lobby and as a scenario file open

The middle of that list — security-review through invitations — is where the project stops being played only by people who know each other. It sits ahead of the things that bring strangers in, onboarding and the daily challenge, rather than behind them, because a consent gap or a missing block is much cheaper to fix before it has been exercised than after.

Continuous #

id title
site-copy Every user-facing string is written by a human
site-a11y The site works at 200% text on a phone
web-testing The app has a test runner, not regexes over source
publishing A post reaches the network in one deploy
render-performance Input-to-pixels stays under the tail
asset-caching Static assets are reused across matches
arena-image The image is small, current and reproducible

Not pursued #

Answered no. A file here is a list of answers rather than a line of work.

id title
declined What lance.blue is not building

Where the schemas are #

There is no lexicon epic. A blue.lance.* or games.permadeath.* schema is a deliverable of whichever epic needs the record, listed in that epic's file, and authored in the lexicons repo. Doing it the other way round — a schema track running ahead of the features — is how you publish a collection name nothing ends up using, and a collection name cannot be taken back once records exist in other people's repositories.

lexicons/PLAN.md was that other way round, and its nine stages are where most of this register came from. Treat it as a source, not as an authority.

Every epic ends in Done #

Open work above, finished work as - [x] items under a trailing ## Done. An epic with nothing under Done says so.

That is what makes "is this finished" answerable by looking rather than by reading: an epic with no - [ ] left is a candidate for complete/.

Where TODO.md went #

This directory replaced it. TODO.md had become a flat list of everything anybody had noticed, with the milestone chain mixed into one-line bugs; every entry in it now lives in the epic that covers it, verbatim where the wording was already right.

There is deliberately no general TODO file any more. Something worth writing down belongs in an epic — and if it belongs to no epic, that is the useful signal, not an excuse to start another list.

Rendering this later #

The frontmatter is shaped for an Astro content collection — the same glob loader web/src/content.config.ts already uses for the blog, with base pointed at this directory and this README excluded from the pattern. Nothing renders it today, and turning it into a public "future development" page is a decision about what the project promises, not a build step.